Merge branch 'main' into pr/bhavesh158/322

This commit is contained in:
Chesterkxng
2026-04-07 13:46:09 +02:00
124 changed files with 3029 additions and 522 deletions
+7
View File
@@ -34,6 +34,7 @@
"dayjs": "^1.11.13",
"fast-xml-parser": "^5.2.5",
"formik": "^2.4.6",
"heic-to": "^1.4.2",
"heic2any": "^0.0.4",
"i18next": "^25.3.2",
"i18next-browser-languagedetector": "^8.2.0",
@@ -7610,6 +7611,12 @@
"node": ">= 0.4"
}
},
"node_modules/heic-to": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/heic-to/-/heic-to-1.4.2.tgz",
"integrity": "sha512-y69thwxfNcEm2Vk8lbOD/cMabnvMJyOREfJYiCHcXCDqlfcPyJoBhyRc8+iDe1B95LRfpbTOpzxzY1xbRkdwBA==",
"license": "LGPL-3.0"
},
"node_modules/heic2any": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/heic2any/-/heic2any-0.0.4.tgz",
+1
View File
@@ -53,6 +53,7 @@
"dayjs": "^1.11.13",
"fast-xml-parser": "^5.2.5",
"formik": "^2.4.6",
"heic-to": "^1.4.2",
"heic2any": "^0.0.4",
"i18next": "^25.3.2",
"i18next-browser-languagedetector": "^8.2.0",
+26
View File
@@ -0,0 +1,26 @@
{
"audioConverter": {
"title": "Audio-Konverter",
"description": "Konvertieren Sie Audiodateien zwischen verschiedenen Formaten.",
"shortDescription": "Konvertieren Sie Audiodateien in verschiedene Formate.",
"longDescription": "Dieses Tool ermöglicht es Ihnen, Audiodateien von einem Format in ein anderes zu konvertieren und unterstützt eine breite Palette von Audioformaten für eine nahtlose Konvertierung.",
"outputFormat": "Ausgabeformat",
"outputFormatDescription": "Wählen Sie das gewünschte Audioausgabeformat",
"inputTitle": "Audioeingabe",
"outputTitle": "Konvertierte Audio"
},
"convertToWebp": {
"description": "Konvertieren Sie verschiedene Bildformate mit anpassbaren Qualitäts- und Hintergrundfarbeneinstellungen in das WebP-Format.",
"shortDescription": "Konvertieren Sie Bilder in WebP-Format mit Qualitätskontrolle.",
"title": "Bilder in WebP konvertieren",
"failedToConvert": "Die Konvertierung des Bildes/der Bilder in WebP ist fehlgeschlagen. Bitte versuchen Sie es erneut.",
"inputTitle": "Eingabebilder",
"resultTitle": "WEBP-Bilder",
"options": {
"title": "Konvertierungseinstellungen",
"quality": "Konvertierungsqualität:",
"qualityDescription": "Höhere Werte = bessere Qualität, größere Dateigröße",
"colorDescription": "Hintergrundfarbe (für transparente Bilder)"
}
}
}
+5
View File
@@ -116,5 +116,10 @@
"longDescription": "Dieses Tool transponiert Komma-getrennte Werte (CSV). Es behandelt die CSV-Datei wie eine Datenmatrix und spiegelt alle Elemente entlang der Hauptdiagonale. Die Ausgabe enthält dieselben CSV-Daten wie die Eingabe, wobei nun alle Zeilen zu Spalten und alle Spalten zu Zeilen werden. Nach der Transposition weist die CSV-Datei entgegengesetzte Dimensionen auf. Wenn die Eingabedatei beispielsweise 4 Spalten und 3 Zeilen enthält, enthält die Ausgabedatei 3 Spalten und 4 Zeilen. Während der Konvertierung bereinigt das Programm die Daten außerdem von unnötigen Zeilen und korrigiert unvollständige Daten. Insbesondere löscht das Tool automatisch alle leeren Datensätze und Kommentare, die mit einem bestimmten Zeichen beginnen, das Sie in den Optionen festlegen können. Sollten die CSV-Daten beschädigt oder verloren gehen, ergänzt das Dienstprogramm die Datei zusätzlich mit leeren Feldern oder benutzerdefinierten Feldern, die Sie in den Optionen festlegen können. CSV-unverständlich!",
"shortDescription": "Transponieren Sie schnell eine CSV-Datei.",
"title": "CSV transponieren"
},
"tsvToJson": {
"description": "Konvertieren Sie TSV-Daten (Tab-Separated Values) in das JSON-Format. Transformieren Sie tabellarische Daten in strukturierte JSON-Objekte.",
"shortDescription": "Konvertieren Sie TSV in das JSON-Format",
"title": "TSV zu JSON"
}
}
-5
View File
@@ -46,11 +46,6 @@
"shortDescription": "Konvertieren Sie Objekte in JSON-Zeichenfolgen",
"title": "Stringify JSON"
},
"tsvToJson": {
"description": "Konvertieren Sie TSV-Daten (Tab-Separated Values) in das JSON-Format. Transformieren Sie tabellarische Daten in strukturierte JSON-Objekte.",
"shortDescription": "Konvertieren Sie TSV in das JSON-Format",
"title": "TSV zu JSON"
},
"validateJson": {
"description": "Überprüfen Sie, ob JSON gültig und wohlgeformt ist.",
"inputTitle": "JSON-Eingabe",
+4 -4
View File
@@ -86,8 +86,8 @@
"longDescription": "Ändern Sie die Ausrichtung von PDF-Seiten, indem Sie sie um 90, 180 oder 270 Grad drehen. Nützlich zum Korrigieren falsch gescannter Dokumente oder zum Vorbereiten von PDFs für den Druck.",
"pageRangesDescription": "Geben Sie Seitenzahlen oder Bereiche durch Kommas getrennt ein (z. B. 1,3,5-7).",
"pageRangesPlaceholder": "z.B. 1,5-8",
"pagesWillBeRotated": "{{count}} Seite{{count !== 1 ? 's' : ''}} wird gedreht",
"pdfPageCount": "PDF hat {{count}} Seite{{count !== 1 ? 's' : ''}}",
"pagesWillBeRotated": "{{count}} Seite{{plural}} wird gedreht",
"pdfPageCount": "PDF hat {{count}} Seite{{plural}}",
"resultTitle": "Gedrehtes PDF",
"rotatingPages": "Seiten drehen",
"rotationAngle": "Drehwinkel",
@@ -103,11 +103,11 @@
"description": "Extrahieren Sie bestimmte Seiten aus einem PDF-Dokument.",
"extractingPages": "Seiten extrahieren",
"inputTitle": "Eingabe-PDF",
"pageExtractionPreview": "{{count}} Seite{{count !== 1 ? 's' : ''}} wird extrahiert",
"pageExtractionPreview": "{{count}} Seite{{plural}} wird extrahiert",
"pageRangesDescription": "Geben Sie Seitenzahlen oder Bereiche durch Kommas getrennt ein (z. B. 1,3,5-7).",
"pageRangesPlaceholder": "z.B. 1,5-8",
"pageSelection": "Seitenauswahl",
"pdfPageCount": "PDF hat {{count}} Seite{{count !== 1 ? 's' : ''}}",
"pdfPageCount": "PDF hat {{count}} Seite{{plural}}",
"resultTitle": "Extrahiertes PDF",
"shortDescription": "Extrahieren Sie bestimmte Seiten aus einer PDF-Datei",
"title": "PDF teilen",
+6 -6
View File
@@ -41,21 +41,21 @@
}
},
"trim": {
"description": "Cut and trim audio files to extract specific segments by specifying start and end times.",
"description": "Cut and trim audio files to extract specific segments by specifying start and end times with sub-second precision.",
"endTime": "End Time",
"endTimeDescription": "End time in format HH:MM:SS (e.g., 00:01:30)",
"endTimeDescription": "End time in format HH:MM:SS.mmm (e.g., 00:01:30.450)",
"inputTitle": "Input Audio",
"longDescription": "This tool allows you to trim audio files by specifying start and end times. You can extract specific segments from longer audio files, remove unwanted parts, or create shorter clips. Supports various audio formats including MP3, AAC, and WAV. Perfect for podcast editing, music production, or any audio editing needs.",
"longDescription": "This tool allows you to trim audio files by specifying start and end times with sub-second precision. You can extract specific segments from longer audio files, remove unwanted parts, or create shorter clips. Supports various audio formats including MP3, AAC, and WAV. Perfect for podcast editing, music production, or any audio editing needs.",
"outputFormat": "Output Format",
"resultTitle": "Trimmed Audio",
"shortDescription": "Trim audio files to extract specific time segments (MP3, AAC, WAV).",
"shortDescription": "Trim audio files to extract specific time segments with sub-second precision (MP3, AAC, WAV).",
"startTime": "Start Time",
"startTimeDescription": "Start time in format HH:MM:SS (e.g., 00:00:30)",
"startTimeDescription": "Start time in format HH:MM:SS.mmm (e.g., 00:00:30.820)",
"timeSettings": "Time Settings",
"title": "Trim Audio",
"toolInfo": {
"title": "What is {{title}}?"
},
"trimmingAudio": "Trimming Audio"
}
}
}
+20 -2
View File
@@ -1,8 +1,26 @@
{
"audioconverter": {
"audioConverter": {
"title": "Audio Converter",
"description": "Convert audio files between different formats.",
"shortDescription": "Convert audio files to various formats.",
"longDescription": "This tool allows you to convert audio files from one format to another, supporting a wide range of audio formats for seamless conversion."
"longDescription": "This tool allows you to convert audio files from one format to another, supporting a wide range of audio formats for seamless conversion.",
"outputFormat": "output Format",
"outputFormatDescription": "Select the desired output audio format",
"inputTitle": "Audio Input",
"outputTitle": "Converted Audio"
},
"convertToWebp": {
"description": "Convert various image formats to WebP format with customizable quality and background color settings.",
"shortDescription": "Convert Images to WebP Format with quality control.",
"title": "Convert Images to WebP",
"failedToConvert": "Conversion of image(s) to WebP failed. Please try again.",
"inputTitle": "Input Images",
"resultTitle": "WEBP Images",
"options": {
"title": "Conversion Settings",
"quality": "Conversion Quality:",
"qualityDescription": "Higher values = better quality, larger file size",
"colorDescription": "Background color (for transparent images)"
}
}
}
+5
View File
@@ -116,5 +116,10 @@
"longDescription": "This tool transposes Comma Separated Values (CSV). It treats the CSV as a matrix of data and flips all elements across the main diagonal. The output contains the same CSV data as the input, but now all the rows have become columns, and all the columns have become rows. After transposition, the CSV file will have opposite dimensions. For example, if the input file has 4 columns and 3 rows, the output file will have 3 columns and 4 rows. During conversion, the program also cleans the data from unnecessary lines and corrects incomplete data. Specifically, the tool automatically deletes all empty records and comments that begin with a specific character, which you can set in the option. Additionally, in cases where the CSV data is corrupted or lost, the utility completes the file with empty fields or custom fields that can be specified in the options. Csv-abulous!",
"shortDescription": "Quickly transpose a CSV file.",
"title": "Transpose CSV"
},
"tsvToJson": {
"description": "Convert TSV (Tab-Separated Values) data to JSON format. Transform tabular data into structured JSON objects.",
"shortDescription": "Convert TSV to JSON format",
"title": "TSV to JSON"
}
}
+40 -17
View File
@@ -10,18 +10,20 @@
"title": "Change image Opacity"
},
"compress": {
"compressedSize": "Compressed Size",
"compressedSize": "Total compressed size",
"compressionOptions": "Compression options",
"description": "Reduce image file size while maintaining quality.",
"failedToCompress": "Failed to compress image. Please try again.",
"compressing": "Compressing images...",
"description": "Reduce the file size of one or multiple images while preserving visual quality. Upload a batch of images and download them all at once as a ZIP archive.",
"failedToCompress": "Image(s) compression failed. Please try again.",
"someFailedToCompress": "Some images could not be compressed and were skipped.",
"fileSizes": "File sizes",
"inputTitle": "Input image",
"maxFileSizeDescription": "Maximum file size in megabytes",
"originalSize": "Original Size",
"qualityDescription": "Image quality percentage (lower means smaller file size)",
"resultTitle": "Compressed image",
"shortDescription": "Compress images to reduce file size while maintaining reasonable quality.",
"title": "Compress Image"
"inputTitle": "Images Input",
"maxFileSizeDescription": "Maximum output file size per image in megabytes",
"originalSize": "Total original size",
"qualityDescription": "Image quality percentage — lower value means smaller file size",
"resultTitle": "Compressed images",
"shortDescription": "Compress one or multiple images at once.",
"title": "Compress Images"
},
"compressPng": {
"description": "This is a program that compresses PNG pictures. As soon as you paste your PNG picture in the input area, the program will compress it and show the result in the output area. In the options, you can adjust the compression level, as well as find the old and new picture file sizes.",
@@ -63,7 +65,28 @@
"qrCode": {
"description": "Generate QR codes for different data types: URL, Text, Email, Phone, SMS, WiFi, vCard, and more.",
"shortDescription": "Create customized QR codes for various data formats.",
"title": "QR Code Generator"
"title": "QR Code Generator",
"resultOutput": "Generated QR code",
"options": {
"qrType": "Select QR Code Type",
"encryptionType": "Encryption Type",
"email": "Email",
"emailBody": "Email Body (optional)",
"emailSubject": "Email Subject (optional)",
"text": "Enter the text",
"name": "Name",
"fullName": "Full Name",
"address": "Address",
"company": "Company (optional)",
"job": "Job Title (optional)",
"website": "Website (optional)",
"message": "Message (optional)",
"phone": "Phone Number",
"url": "Enter the URL",
"ssid": "Network Name (SSID)",
"password": "Password",
"size": "Size in pixels (100-1000)"
}
},
"removeBackground": {
"description": "World",
@@ -71,10 +94,10 @@
"title": "Remove Background from Image"
},
"resize": {
"description": "Resize images to different dimensions.",
"description": "Resize images using pixel dimensions or percentage scaling. Maintain aspect ratio and set width or height with automatic adjustment. Download results individually or as a ZIP archive.",
"dimensionType": "Dimension Type",
"heightDescription": "Height (in pixels)",
"inputTitle": "Input Image",
"inputTitle": "Input Images",
"maintainAspectRatio": "Maintain Aspect Ratio",
"maintainAspectRatioDescription": "Maintain the original aspect ratio of the image.",
"percentage": "Percentage",
@@ -84,16 +107,16 @@
"resizeByPixels": "Resize by Pixels",
"resizeByPixelsDescription": "Resize by specifying dimensions in pixels.",
"resizeMethod": "Resize Method",
"resultTitle": "Resized Image",
"resultTitle": "Resized Image(s)",
"setHeight": "Set Height",
"setHeightDescription": "Specify the height in pixels and calculate width based on aspect ratio.",
"setWidth": "Set Width",
"setWidthDescription": "Specify the width in pixels and calculate height based on aspect ratio.",
"shortDescription": "Resize images easily.",
"shortDescription": "Resize one or multiple images at once.",
"title": "Resize Image",
"toolInfo": {
"description": "This tool allows you to resize JPG, PNG, SVG, or GIF images. You can resize by specifying dimensions in pixels or by percentage, with options to maintain the original aspect ratio.",
"title": "Resize Image"
"description": "This tool allows you to resize one or multiple JPG, PNG, SVG, or GIF images at once. Upload a single image or a batch of images and resize them by specifying dimensions in pixels or by percentage, with options to maintain the original aspect ratio. When processing multiple images, results are bundled into a ZIP file for easy download.",
"title": "Resize Image(s)"
},
"widthDescription": "Width (in pixels)"
},
+26
View File
@@ -51,6 +51,32 @@
"shortDescription": "Convert TSV to JSON format",
"title": "TSV to JSON"
},
"jsonToCsv": {
"description": "Convert JSON data to CSV format. Supports nested objects and arrays, automatic header detection, and configurable delimiters and quoting — ready for Excel, Google Sheets, or any spreadsheet application.",
"shortDescription": "Convert JSON to CSV format",
"title": "JSON to CSV",
"quotingOption": "String Quoting",
"delimiterOption": "Delimiter",
"headerOption": "Headers",
"inputTitle": "Input JSON",
"outputTitle": "Output CSV",
"options": {
"delimiter": "Character used to separate values in each row (e.g. comma, semicolon, tab)",
"alwaysQuote": {
"label": "Always quote",
"description": "Wrap every cell in double-quotes, regardless of its content."
},
"autoQuote": {
"label": "Auto (recommended)",
"description": "Only quote cells that contain the delimiter, a double-quote, or a newline. Keeps output clean and minimal."
},
"header": {
"label": "Include header row",
"description": "Use JSON keys as column names in the first row. Recommended for readability and import compatibility."
}
}
},
"validateJson": {
"description": "Check if JSON is valid and well-formed.",
"inputTitle": "Input JSON",
+18 -6
View File
@@ -65,8 +65,20 @@
},
"convertToPdf": {
"title": "Images to PDF",
"description": "Convert various image formats (PNG, GIF, JPG, TIF, PSD, SVG, WEBP, HEIC, RAW) to PDF, with options to scale the image and choose page orientation.",
"shortDescription": "Convert images to PDF with scale and orientation control"
"description": "Convert various image formats (PNG, GIF, JPG, WEBP, HEIC, HEIF) to PDF, with options to scale the image and choose page orientation.",
"shortDescription": "Convert images to PDF with scale and orientation control",
"inputTitle": "Input Images",
"resultTitle": "Output PDF",
"options": {
"type": "PDF Type",
"orientation": "Orientation",
"scale": "Scale",
"portrait": "Portrait (Vertical)",
"landscape": "Landscape (Horizontal)",
"fullsize": "Full Size (Image size)",
"a4": "A4 Page",
"image": "Image"
}
},
"protectPdf": {
"description": "Add password protection to your PDF files securely in your browser",
@@ -86,8 +98,8 @@
"longDescription": "Change the orientation of PDF pages by rotating them 90, 180, or 270 degrees. Useful for fixing incorrectly scanned documents or preparing PDFs for printing.",
"pageRangesDescription": "Enter page numbers or ranges separated by commas (e.g., 1,3,5-7)",
"pageRangesPlaceholder": "e.g., 1,5-8",
"pagesWillBeRotated": "{{count}} page{{count !== 1 ? 's' : ''}} will be rotated",
"pdfPageCount": "PDF has {{count}} page{{count !== 1 ? 's' : ''}}",
"pagesWillBeRotated": "{{count}} page{{plural}} will be rotated",
"pdfPageCount": "PDF has {{count}} page{{plural}}",
"resultTitle": "Rotated PDF",
"rotatingPages": "Rotating pages",
"rotationAngle": "Rotation Angle",
@@ -103,11 +115,11 @@
"description": "Extract specific pages from a PDF document.",
"extractingPages": "Extracting pages",
"inputTitle": "Input PDF",
"pageExtractionPreview": "{{count}} page{{count !== 1 ? 's' : ''}} will be extracted",
"pageExtractionPreview": "{{count}} page{{ plural }} will be extracted",
"pageRangesDescription": "Enter page numbers or ranges separated by commas (e.g., 1,3,5-7)",
"pageRangesPlaceholder": "e.g., 1,5-8",
"pageSelection": "Page Selection",
"pdfPageCount": "PDF has {{count}} page{{count !== 1 ? 's' : ''}}",
"pdfPageCount": "PDF has {{count}} page{{plural}}",
"resultTitle": "Extracted PDF",
"shortDescription": "Extract specific pages from a PDF file",
"title": "Split PDF",
+27
View File
@@ -119,5 +119,32 @@
"description": "Convert a formatted time duration (HH:MM:SS) into a decimal hour value.",
"shortDescription": "Convert human time to decimal time",
"longDescription": "Convert a formatted time string (HH:MM:SS or HH:MM) into its decimal-hour equivalent. Hours can be any positive number, while minutes and seconds accept values from 0-59 and can be single or double digits (e.g., '12:5' or '12:05'). This function interprets hours, minutes, and seconds, then calculates the total duration as a single decimal value. It is useful for productivity tracking, payroll calculations, time-based billing, data analysis, or any workflow that requires converting human-readable time into a numerical format that can be easily summed, compared, or processed. For example, '03:26:00' becomes 3.43 hours, and '12:5' becomes 12.08 hours."
},
"discordTimestamp": {
"name": "Discord Timestamp Generator",
"description": "Convert dates and times into Discord's special timestamp format that automatically adapts to each user's local timezone and language",
"shortDescription": "Generate Discord timestamps from dates",
"inputTitle": "Input Datetimes",
"outputTitle": "Discord Timestamps",
"formats": {
"title": "Timestamp Format",
"description": "Choose how the timestamp will appear in Discord — hover over each option to see an example",
"short_time": "Short Time (ex: 14:30)",
"long_time": "Long Time (ex: 14:30:00)",
"short_date": "Short Date (ex: 01/01/2025)",
"long_date": "Long Date (ex: 1 January 2025)",
"short_datetime": "Short Date & Time (ex: 1 January 2025 14:30)",
"long_datetime": "Long Date & Time (ex: Monday, 1 January 2025 14:30)",
"relative": "Relative (ex: 2 hours ago)"
},
"utc": {
"title": "Input Options",
"label": "Enforce UTC",
"description": "Treat input datetimes as UTC. Disable to use your local timezone instead"
},
"toolInfo": {
"title": "What is a Discord Timestamp?",
"description": "Discord timestamps are a special syntax that displays dates and times dynamically. Instead of showing a fixed time, they automatically convert to each viewer's local timezone and language — so everyone sees the correct time for their location. Simply enter one datetime per line, choose a format, and paste the results directly into Discord. The tool processes each line independently — blank lines are preserved in the output, invalid lines are flagged with ❌, and valid datetimes are converted instantly. Enable UTC mode to ensure consistent results regardless of your local timezone."
}
}
}
+4
View File
@@ -260,6 +260,10 @@
"loading": "Loading... This may take a moment.",
"result": "Result"
},
"toolMultipleImageInput": {
"inputTitle": "Images Input",
"noFilesSelected": "No files selected"
},
"userTypes": {
"developers": "Developers",
"generalUsers": "General users"
+29
View File
@@ -116,5 +116,34 @@
"description": "Convert video files to animated GIF format. Extract specific time ranges and create shareable animated images.",
"shortDescription": "Convert video to animated GIF",
"title": "Video to GIF"
},
"addAudio": {
"audioFile": "Audio File",
"audioInputTitle": "Input Audio",
"description": "Easily add an audio track to any video file. You can choose to replace the original audio entirely or mix a new track with the existing one. Adjust the volume, set looping options, and trim the audio with precise start and end times. Supports all common video and audio formats for flexible editing and quick results.",
"loadingText": "Adding audio to video...",
"mode": "Audio Mode",
"modeMix": "Mix (Keep Original + Add New)",
"modeReplace": "Replace Original Audio",
"resultTitle": "Processed Video",
"shortDescription": "Add or replace audio track in video",
"title": "Add Audio to Video",
"videoInputTitle": "Input Video",
"volume": "New Audio Volume",
"volumeDesc": "Volume level for the new audio track.",
"loopOption": "Loop options",
"loop": "Loop (repeat audio until video end or defined end time)",
"noLoop": "No Loop (play audio only once)",
"timing": "Audio Timing",
"durationMode": "Time Range",
"timingDefault": "Default (full video)",
"timingStart": "Set Start Time",
"timingEnd": "Set End Time",
"timingStartEnd": "Set Start & End Time",
"timingModeDesc": "Choose when the new audio plays within the video timeline.",
"startTime": "Start Time",
"startTimeDesc": "When the new audio starts playing (HH:MM:SS.mm).",
"endTime": "End Time",
"endTimeDesc": "When the new audio stops playing (HH:MM:SS.mm)."
}
}
+26
View File
@@ -0,0 +1,26 @@
{
"audioConverter": {
"title": "Convertidor de Audio",
"description": "Convierte archivos de audio entre diferentes formatos.",
"shortDescription": "Convierte archivos de audio a varios formatos.",
"longDescription": "Esta herramienta le permite convertir archivos de audio de un formato a otro, con soporte para una amplia gama de formatos de audio para una conversión perfecta.",
"outputFormat": "Formato de Salida",
"outputFormatDescription": "Seleccione el formato de audio de salida deseado",
"inputTitle": "Entrada de Audio",
"outputTitle": "Audio Convertido"
},
"convertToWebp": {
"description": "Convierte varios formatos de imagen a formato WebP con configuración de calidad y color de fondo personalizables.",
"shortDescription": "Convierte imágenes a formato WebP con control de calidad.",
"title": "Convertir imágenes a WebP",
"failedToConvert": "La conversión de la(s) imagen(es) a WebP falló. Por favor, inténtelo de nuevo.",
"inputTitle": "Imágenes de entrada",
"resultTitle": "Imágenes WEBP",
"options": {
"title": "Configuración de conversión",
"quality": "Calidad de conversión:",
"qualityDescription": "Valores más altos = mejor calidad, tamaño de archivo más grande",
"colorDescription": "Color de fondo (para imágenes transparentes)"
}
}
}
+5
View File
@@ -116,5 +116,10 @@
"longDescription": "Esta herramienta transpone valores separados por comas (CSV). Trata el CSV como una matriz de datos e invierte todos los elementos a lo largo de la diagonal principal. El resultado contiene los mismos datos CSV que la entrada, pero ahora todas las filas se han convertido en columnas y todas las columnas en filas. Tras la transposición, el archivo CSV tendrá dimensiones opuestas. Por ejemplo, si el archivo de entrada tiene 4 columnas y 3 filas, el archivo de salida tendrá 3 columnas y 4 filas. Durante la conversión, el programa también elimina las líneas innecesarias y corrige los datos incompletos. En concreto, la herramienta elimina automáticamente todos los registros vacíos y los comentarios que empiezan por un carácter específico, que se puede configurar en la opción. Además, si los datos CSV se corrompen o se pierden, la utilidad completa el archivo con campos vacíos o campos personalizados que se pueden especificar en las opciones. ¡Csv-abuloso!",
"shortDescription": "Transponer rápidamente un archivo CSV.",
"title": "Transponer CSV"
},
"tsvToJson": {
"description": "Convierte datos TSV (Valores Separados por Tabulaciones) a formato JSON. Transforma datos tabulares en objetos JSON estructurados.",
"shortDescription": "Convertir TSV a formato JSON",
"title": "TSV a JSON"
}
}
-5
View File
@@ -46,11 +46,6 @@
"shortDescription": "Convertir objetos en cadena JSON",
"title": "Convertir JSON en cadenas"
},
"tsvToJson": {
"description": "Convierte datos TSV (Valores Separados por Tabulaciones) a formato JSON. Transforma datos tabulares en objetos JSON estructurados.",
"shortDescription": "Convertir TSV a formato JSON",
"title": "TSV a JSON"
},
"validateJson": {
"description": "Comprueba si JSON es válido y está bien formado.",
"inputTitle": "Entrada JSON",
+4 -4
View File
@@ -86,8 +86,8 @@
"longDescription": "Cambie la orientación de las páginas PDF girándolas 90, 180 o 270 grados. Útil para corregir documentos escaneados incorrectamente o preparar archivos PDF para imprimir.",
"pageRangesDescription": "Introduzca números de página o rangos separados por comas (por ejemplo, 1, 3, 5-7)",
"pageRangesPlaceholder": "p. ej., 1,5-8",
"pagesWillBeRotated": "{{count}} página{{count !== 1 ? 's' : ''}} se rotará",
"pdfPageCount": "PDF tiene {{count}} página{{count !== 1 ? 's' : ''}}",
"pagesWillBeRotated": "{{count}} página{{plural}} se rotará",
"pdfPageCount": "PDF tiene {{count}} página{{plural}}",
"resultTitle": "PDF rotado",
"rotatingPages": "Páginas rotatorias",
"rotationAngle": "Ángulo de rotación",
@@ -103,11 +103,11 @@
"description": "Extraer páginas específicas de un documento PDF.",
"extractingPages": "Extrayendo páginas",
"inputTitle": "PDF de entrada",
"pageExtractionPreview": "{{count}} página{{count !== 1 ? 's' : ''}} se extraerán",
"pageExtractionPreview": "{{count}} página{{ plural }} se extraerán",
"pageRangesDescription": "Introduzca números de página o rangos separados por comas (por ejemplo, 1, 3, 5-7)",
"pageRangesPlaceholder": "p. ej., 1,5-8",
"pageSelection": "Selección de página",
"pdfPageCount": "PDF tiene {{count}} página{{count !== 1 ? 's' : ''}}",
"pdfPageCount": "PDF tiene {{count}} página{{plural}}",
"resultTitle": "PDF extraído",
"shortDescription": "Extraer páginas específicas de un archivo PDF",
"title": "PDF dividido",
+26
View File
@@ -0,0 +1,26 @@
{
"audioConverter": {
"title": "Convertisseur Audio",
"description": "Convertissez des fichiers audio entre différents formats.",
"shortDescription": "Convertissez des fichiers audio en divers formats.",
"longDescription": "Cet outil vous permet de convertir des fichiers audio d'un format à un autre, en support une large gamme de formats audio pour une conversion transparente.",
"outputFormat": "Format de Sortie",
"outputFormatDescription": "Sélectionnez le format audio de sortie souhaité",
"inputTitle": "Entrée Audio",
"outputTitle": "Audio Converti"
},
"convertToWebp": {
"description": "Convertissez divers formats d'image au format WebP avec des paramètres de qualité et de couleur de fond personnalisables.",
"shortDescription": "Convertissez les images au format WebP avec contrôle de la qualité.",
"title": "Convertir des images en WebP",
"failedToConvert": "La conversion de l'image ou des images en WebP a échoué. Veuillez réessayer.",
"inputTitle": "Images d'entrée",
"resultTitle": "Images WEBP",
"options": {
"title": "Paramètres de conversion",
"quality": "Qualité de conversion:",
"qualityDescription": "Valeurs plus élevées = meilleure qualité, taille de fichier plus grande",
"colorDescription": "Couleur de fond (pour les images transparentes)"
}
}
}
+5
View File
@@ -116,5 +116,10 @@
"longDescription": "Cet outil transpose les fichiers CSV (valeurs séparées par des virgules). Il traite le fichier CSV comme une matrice de données et inverse tous les éléments sur la diagonale principale. Le fichier CSV de sortie contient les mêmes données CSV que le fichier d'entrée, mais toutes les lignes sont désormais des colonnes, et toutes les colonnes des lignes. Après la transposition, le fichier CSV aura des dimensions opposées. Par exemple, si le fichier d'entrée comporte 4 colonnes et 3 lignes, le fichier de sortie comportera 3 colonnes et 4 lignes. Lors de la conversion, le programme supprime également les lignes inutiles et corrige les données incomplètes. Plus précisément, l'outil supprime automatiquement tous les enregistrements vides et les commentaires commençant par un caractère spécifique, que vous pouvez définir dans l'option. De plus, en cas de corruption ou de perte des données CSV, l'utilitaire complète le fichier avec des champs vides ou personnalisés, que vous pouvez spécifier dans les options. Csv-abulous !",
"shortDescription": "Transposer rapidement un fichier CSV.",
"title": "Transposer CSV"
},
"tsvToJson": {
"description": "Convertissez les données TSV (valeurs séparées par des tabulations) au format JSON. Transformez les données tabulaires en objets JSON structurés.",
"shortDescription": "Convertir le format TSV au format JSON",
"title": "TSV vers JSON"
}
}
-5
View File
@@ -46,11 +46,6 @@
"shortDescription": "Convertir des objets en chaîne JSON",
"title": "Stringify JSON"
},
"tsvToJson": {
"description": "Convertissez les données TSV (valeurs séparées par des tabulations) au format JSON. Transformez les données tabulaires en objets JSON structurés.",
"shortDescription": "Convertir le format TSV au format JSON",
"title": "TSV vers JSON"
},
"validateJson": {
"description": "Vérifiez si JSON est valide et bien formé.",
"inputTitle": "Entrée JSON",
+4 -4
View File
@@ -86,8 +86,8 @@
"longDescription": "Modifiez l'orientation des pages PDF en les faisant pivoter de 90, 180 ou 270 degrés. Utile pour corriger des documents mal numérisés ou préparer des PDF pour l'impression.",
"pageRangesDescription": "Saisissez les numéros de page ou les plages séparés par des virgules (par exemple, 1, 3, 5-7)",
"pageRangesPlaceholder": "par exemple, 1,5-8",
"pagesWillBeRotated": "{{count}} page{{count !== 1 ? 's' : ''}} sera tourné",
"pdfPageCount": "PDF a {{count}} page{{count !== 1 ? 's' : ''}}",
"pagesWillBeRotated": "{{count}} page{{plural}} sera tourné",
"pdfPageCount": "PDF a {{count}} page{{plural}}",
"resultTitle": "PDF pivoté",
"rotatingPages": "Rotation des pages",
"rotationAngle": "Angle de rotation",
@@ -103,11 +103,11 @@
"description": "Extraire des pages spécifiques d'un document PDF.",
"extractingPages": "Extraire les pages",
"inputTitle": "Entrée PDF",
"pageExtractionPreview": "{{count}} page{{count !== 1 ? 's' : ''}} sera(ont) extrait(s)",
"pageExtractionPreview": "{{count}} page{{plural}} sera(ont) extrait(s)",
"pageRangesDescription": "Saisissez les numéros de page ou les plages séparés par des virgules (par exemple, 1, 3, 5-7)",
"pageRangesPlaceholder": "par exemple, 1,5-8",
"pageSelection": "Sélection de page",
"pdfPageCount": "PDF a {{count}} page{{count !== 1 ? 's' : ''}}",
"pdfPageCount": "PDF a {{count}} page{{plural}}",
"resultTitle": "PDF extrait",
"shortDescription": "Extraire des pages spécifiques d'un fichier PDF",
"title": "Diviser le PDF",
+26
View File
@@ -0,0 +1,26 @@
{
"audioConverter": {
"title": "ऑडियो कनवर्टर",
"description": "ऑडियो फ़ाइलों को विभिन्न प्रारूपों के बीच रूपांतरित करें।",
"shortDescription": "ऑडियो फ़ाइलों को विभिन्न प्रारूपों में रूपांतरित करें।",
"longDescription": "यह उपकरण आपको ऑडियो फ़ाइलों को एक प्रारूप से दूसरे में रूपांतरित करने की अनुमति देता है, निरंतर रूपांतरण के लिए ऑडियो प्रारूपों की एक विस्तृत श्रृंखला का समर्थन करता है।",
"outputFormat": "आउटपुट प्रारूप",
"outputFormatDescription": "वांछित आउटपुट ऑडियो प्रारूप का चयन करें",
"inputTitle": "ऑडियो इनपुट",
"outputTitle": "परिवर्तित ऑडियो"
},
"convertToWebp": {
"description": "विभिन्न छवि प्रारूपों को अनुकूलन योग्य गुणवत्ता और पृष्ठभूमि रंग सेटिंग के साथ WebP प्रारूप में रूपांतरित करें।",
"shortDescription": "गुणवत्ता नियंत्रण के साथ छवियों को WebP प्रारूप में रूपांतरित करें।",
"title": "छवियों को WebP में रूपांतरित करें",
"failedToConvert": "छवि(यों) को WebP में रूपांतरण विफल रहा। कृपया पुनः प्रयास करें।",
"inputTitle": "इनपुट छवियां",
"resultTitle": "WEBP छवियां",
"options": {
"title": "रूपांतरण सेटिंग",
"quality": "रूपांतरण गुणवत्ता:",
"qualityDescription": "उच्च मान = बेहतर गुणवत्ता, बड़ी फ़ाइल आकार",
"colorDescription": "पृष्ठभूमि रंग (पारदर्शी छवियों के लिए)"
}
}
}
+13
View File
@@ -214,5 +214,18 @@
"shortDescription": "CSV फ़ाइल को शीघ्रता से ट्रांसपोज़ करें.",
"title": "CSV ट्रांसपोज़ करें",
"transposeOptions": "ट्रांसपोज़ विकल्प"
},
"tsvToJson": {
"arrayFormat": "सरणी प्रारूप",
"conversionOptions": "रूपांतरण विकल्प",
"description": "TSV डेटा को JSON प्रारूप में बदलें।",
"firstRowAsHeaders": "पहली पंक्ति शीर्षक के रूप में",
"includeHeaders": "शीर्षक शामिल करें",
"inputTitle": "इनपुट TSV",
"objectFormat": "ऑब्जेक्ट प्रारूप",
"outputFormat": "आउटपुट प्रारूप",
"resultTitle": "JSON परिणाम",
"shortDescription": "TSV को JSON प्रारूप में परिवर्तित करें",
"title": "TSV से JSON"
}
}
-13
View File
@@ -82,19 +82,6 @@
"stringifyOptions": "स्ट्रिंगिफाई विकल्प",
"title": "JSON स्ट्रिंगिफाई"
},
"tsvToJson": {
"arrayFormat": "सरणी प्रारूप",
"conversionOptions": "रूपांतरण विकल्प",
"description": "TSV डेटा को JSON प्रारूप में बदलें।",
"firstRowAsHeaders": "पहली पंक्ति शीर्षक के रूप में",
"includeHeaders": "शीर्षक शामिल करें",
"inputTitle": "इनपुट TSV",
"objectFormat": "ऑब्जेक्ट प्रारूप",
"outputFormat": "आउटपुट प्रारूप",
"resultTitle": "JSON परिणाम",
"shortDescription": "TSV को JSON प्रारूप में परिवर्तित करें",
"title": "TSV से JSON"
},
"validateJson": {
"allowComments": "टिप्पणियां अनुमति दें",
"allowTrailingCommas": "अनुगामी कॉमा अनुमति दें",
+4 -4
View File
@@ -122,8 +122,8 @@
"pageRangesDescription": "पृष्ठ संख्या या श्रेणियाँ अल्पविराम से अलग करके दर्ज करें (उदाहरण के लिए, 1,3,5-7)",
"pageRangesPlaceholder": "उदाहरणार्थ, 1,5-8",
"pagesPlaceholder": "जैसे 1,3-5,7",
"pagesWillBeRotated": "{{count}} पृष्ठ{{count !== 1 ? 's' : ''}} घुमाया जाएगा",
"pdfPageCount": "पीडीएफ में है {{count}} पृष्ठ{{count !== 1 ? 's' : ''}}",
"pagesWillBeRotated": "{{count}} पृष्ठ घुमाया जाएगा",
"pdfPageCount": "पीडीएफ में है {{count}} पृष्ठ",
"resultTitle": "घुमाया गया PDF",
"rotatingPages": "घूमते हुए पृष्ठ",
"rotationAngle": "घुमाने का कोण",
@@ -142,12 +142,12 @@
"extractingPages": "पृष्ठ निकालना",
"includeBookmarks": "बुकमार्क शामिल करें",
"inputTitle": "इनपुट PDF",
"pageExtractionPreview": "{{count}} पृष्ठ{{count !== 1 ? 's' : ''}} निकाला जाएगा",
"pageExtractionPreview": "{{count}} पृष्ठ निकाला जाएगा",
"pageRanges": "पेज श्रेणियां",
"pageRangesDescription": "पृष्ठ संख्या या श्रेणियाँ अल्पविराम से अलग करके दर्ज करें (उदाहरण के लिए, 1,3,5-7)",
"pageRangesPlaceholder": "उदाहरणार्थ, 1,5-8",
"pageSelection": "पृष्ठ चयन",
"pdfPageCount": "पीडीएफ में है {{count}} पृष्ठ{{count !== 1 ? 's' : ''}}",
"pdfPageCount": "पीडीएफ में है {{count}} पृष्ठ",
"rangesPlaceholder": "जैसे 1,3-5,7",
"resultTitle": "विभाजित PDF फ़ाइलें",
"shortDescription": "पीडीएफ फ़ाइल से विशिष्ट पेज निकालें",
+26
View File
@@ -0,0 +1,26 @@
{
"audioConverter": {
"title": "オーディオコンバーター",
"description": "オーディオファイルを異なるフォーマット間で変換します。",
"shortDescription": "オーディオファイルをさまざまなフォーマットに変換します。",
"longDescription": "このツールは、オーディオファイルを1つのフォーマットから別のフォーマットに変換でき、シームレスな変換のための幅広いオーディオフォーマットをサポートしています。",
"outputFormat": "出力フォーマット",
"outputFormatDescription": "目的の出力オーディオフォーマットを選択してください",
"inputTitle": "オーディオ入力",
"outputTitle": "変換済みオーディオ"
},
"convertToWebp": {
"description": "カスタマイズ可能な品質と背景色の設定で、さまざまな画像形式をWebP形式に変換します。",
"shortDescription": "品質制御を備えた画像をWebP形式に変換します。",
"title": "画像をWebPに変換",
"failedToConvert": "画像のWebPへの変換に失敗しました。もう一度やり直してください。",
"inputTitle": "入力画像",
"resultTitle": "WEBP画像",
"options": {
"title": "変換設定",
"quality": "変換品質:",
"qualityDescription": "値が高いほど品質が高く、ファイルサイズが大きくなります",
"colorDescription": "背景色(透明な画像用)"
}
}
}
+5
View File
@@ -116,5 +116,10 @@
"longDescription": "このツールは、カンマ区切り値(CSV)を転置します。CSVをデータの行列として扱い、すべての要素を主対角線を挟んで反転します。出力には入力と同じCSVデータが含まれますが、すべての行が列になり、すべての列が行になります。転置後、CSVファイルの次元は逆になります。たとえば、入力ファイルに4列3行がある場合、出力ファイルは3列4行になります。変換中に、プログラムは不要な行のデータを削除し、不完全なデータを修正します。具体的には、ツールはオプションで設定できる特定の文字で始まるすべての空のレコードとコメントを自動的に削除します。さらに、CSVデータが破損または失われた場合、ユーティリティはオプションで指定できる空のフィールドまたはカスタムフィールドでファイルを補完します。CSV-abulous!",
"shortDescription": "CSV ファイルをすばやく転置します。",
"title": "CSV転置"
},
"tsvToJson": {
"description": "TSV(タブ区切り値)データをJSON形式に変換します。表形式のデータを構造化されたJSONオブジェクトに変換します。",
"shortDescription": "TSVをJSON形式に変換する",
"title": "TSVからJSONへ"
}
}
-5
View File
@@ -46,11 +46,6 @@
"shortDescription": "オブジェクトをJSON文字列に変換する",
"title": "JSONを文字列化する"
},
"tsvToJson": {
"description": "TSV(タブ区切り値)データをJSON形式に変換します。表形式のデータを構造化されたJSONオブジェクトに変換します。",
"shortDescription": "TSVをJSON形式に変換する",
"title": "TSVからJSONへ"
},
"validateJson": {
"description": "JSON が有効かつ整形式であるかどうかを確認します。",
"inputTitle": "入力JSON",
+4 -4
View File
@@ -86,8 +86,8 @@
"longDescription": "PDFページの向きを90度、180度、または270度回転して変更できます。スキャンミスを修正したり、印刷用のPDFを準備したりする際に便利です。",
"pageRangesDescription": "ページ番号または範囲をコンマで区切って入力してください(例:1,3,5-7)",
"pageRangesPlaceholder": "例:1.5-8",
"pagesWillBeRotated": "{{count}} ページ{{count !== 1 ? 's' : ''}} 回転します",
"pdfPageCount": "PDFには {{count}} ページ{{count !== 1 ? 's' : ''}}",
"pagesWillBeRotated": "{{count}} ページ 回転します",
"pdfPageCount": "PDFには {{count}} ページ",
"resultTitle": "回転したPDF",
"rotatingPages": "ページの回転",
"rotationAngle": "回転角度",
@@ -103,11 +103,11 @@
"description": "PDF ドキュメントから特定のページを抽出します。",
"extractingPages": "ページの抽出",
"inputTitle": "入力PDF",
"pageExtractionPreview": "{{count}} ページ{{count !== 1 ? 's' : ''}} 抽出されます",
"pageExtractionPreview": "{{count}} ページ 抽出されます",
"pageRangesDescription": "ページ番号または範囲をコンマで区切って入力してください(例:1,3,5-7)",
"pageRangesPlaceholder": "例:1.5-8",
"pageSelection": "ページ選択",
"pdfPageCount": "PDFには {{count}} ページ{{count !== 1 ? 's' : ''}}",
"pdfPageCount": "PDFには {{count}} ページ",
"resultTitle": "抽出されたPDF",
"shortDescription": "PDFファイルから特定のページを抽出する",
"title": "PDFを分割",
+26
View File
@@ -0,0 +1,26 @@
{
"audioConverter": {
"title": "Audio Converter",
"description": "Converteer audiobestanden tussen verschillende indelingen.",
"shortDescription": "Converteer audiobestanden naar verschillende indelingen.",
"longDescription": "Met deze tool kunt u audiobestanden van het ene naar het andere formaat converteren, met ondersteuning voor een breed scala aan audioformaten voor naadloze conversie.",
"outputFormat": "Uitvoerindeling",
"outputFormatDescription": "Selecteer het gewenste audio-uitvoerformat",
"inputTitle": "Audio-invoer",
"outputTitle": "Geconverteerde Audio"
},
"convertToWebp": {
"description": "Converteer verschillende afbeeldingsindelingen naar WebP-indeling met aanpasbare instellingen voor kwaliteit en achtergrondkleur.",
"shortDescription": "Converteer afbeeldingen naar WebP-indeling met kwaliteitsbeheer.",
"title": "Afbeeldingen naar WebP converteren",
"failedToConvert": "Conversie van afbeelding(en) naar WebP mislukt. Probeer het opnieuw.",
"inputTitle": "Invoerafbeeldingen",
"resultTitle": "WEBP-afbeeldingen",
"options": {
"title": "Conversie-instellingen",
"quality": "Conversiekwaliteit:",
"qualityDescription": "Hogere waarden = betere kwaliteit, groter bestandsformaat",
"colorDescription": "Achtergrondkleur (voor transparante afbeeldingen)"
}
}
}
+5
View File
@@ -116,5 +116,10 @@
"longDescription": "Deze tool transponeert Comma Separated Values (CSV). Het behandelt de CSV als een matrix met data en draait alle elementen om over de hoofddiagonaal. De uitvoer bevat dezelfde CSV-data als de invoer, maar nu zijn alle rijen kolommen en alle kolommen rijen geworden. Na transpositie heeft het CSV-bestand tegengestelde afmetingen. Als het invoerbestand bijvoorbeeld 4 kolommen en 3 rijen heeft, heeft het uitvoerbestand 3 kolommen en 4 rijen. Tijdens de conversie zuivert het programma de data ook van onnodige regels en corrigeert het onvolledige data. De tool verwijdert automatisch alle lege records en opmerkingen die beginnen met een specifiek teken, dat u in de optie kunt instellen. Bovendien, in gevallen waarin de CSV-data beschadigd of verloren zijn, vult het hulpprogramma het bestand aan met lege velden of aangepaste velden die u in de opties kunt opgeven. Csv-abulous!",
"shortDescription": "Snel een CSV-bestand transponeren.",
"title": "CSV transponeren"
},
"tsvToJson": {
"description": "Converteer TSV-gegevens (Tab-Separated Values) naar JSON-formaat. Transformeer tabelgegevens naar gestructureerde JSON-objecten.",
"shortDescription": "Converteer TSV naar JSON-formaat",
"title": "TSV naar JSON"
}
}
-5
View File
@@ -46,11 +46,6 @@
"shortDescription": "Objecten converteren naar JSON-string",
"title": "Stringify JSON"
},
"tsvToJson": {
"description": "Converteer TSV-gegevens (Tab-Separated Values) naar JSON-formaat. Transformeer tabelgegevens naar gestructureerde JSON-objecten.",
"shortDescription": "Converteer TSV naar JSON-formaat",
"title": "TSV naar JSON"
},
"validateJson": {
"description": "Controleer of JSON geldig en correct is geformuleerd.",
"inputTitle": "Invoer JSON",
+4 -4
View File
@@ -86,8 +86,8 @@
"longDescription": "Wijzig de oriëntatie van PDF-pagina's door ze 90, 180 of 270 graden te draaien. Handig voor het corrigeren van onjuist gescande documenten of het voorbereiden van PDF's voor afdrukken.",
"pageRangesDescription": "Voer paginanummers of bereiken in, gescheiden door komma's (bijv. 1, 3, 5-7)",
"pageRangesPlaceholder": "bijv. 1,5-8",
"pagesWillBeRotated": "{{count}} pagina{{count !== 1 ? 's' : ''}} zal worden gedraaid",
"pdfPageCount": "PDF heeft {{count}} pagina{{count !== 1 ? 's' : ''}}",
"pagesWillBeRotated": "{{count}} pagina{{plural}} zal worden gedraaid",
"pdfPageCount": "PDF heeft {{count}} pagina{{plural}}",
"resultTitle": "Gedraaide PDF",
"rotatingPages": "Roterende pagina's",
"rotationAngle": "Rotatiehoek",
@@ -103,11 +103,11 @@
"description": "Specifieke pagina's uit een PDF-document extraheren.",
"extractingPages": "Pagina's extraheren",
"inputTitle": "PDF-invoer",
"pageExtractionPreview": "{{count}} pagina{{count !== 1 ? 's' : ''}} zal worden geëxtraheerd",
"pageExtractionPreview": "{{count}} pagina{{ plural }} zal worden geëxtraheerd",
"pageRangesDescription": "Voer paginanummers of bereiken in, gescheiden door komma's (bijv. 1, 3, 5-7)",
"pageRangesPlaceholder": "bijv. 1,5-8",
"pageSelection": "Paginaselectie",
"pdfPageCount": "PDF heeft {{count}} pagina{{count !== 1 ? 's' : ''}}",
"pdfPageCount": "PDF heeft {{count}} pagina{{plural}}",
"resultTitle": "Geëxtraheerde PDF",
"shortDescription": "Specifieke pagina's uit een PDF-bestand extraheren",
"title": "PDF splitsen",
+26
View File
@@ -0,0 +1,26 @@
{
"audioConverter": {
"title": "Conversor de Áudio",
"description": "Converta arquivos de áudio entre diferentes formatos.",
"shortDescription": "Converta arquivos de áudio para vários formatos.",
"longDescription": "Esta ferramenta permite que você converta arquivos de áudio de um formato para outro, suportando uma ampla gama de formatos de áudio para conversão perfeita.",
"outputFormat": "Formato de Saída",
"outputFormatDescription": "Selecione o formato de áudio de saída desejado",
"inputTitle": "Entrada de Áudio",
"outputTitle": "Áudio Convertido"
},
"convertToWebp": {
"description": "Converta vários formatos de imagem para o formato WebP com configurações personalizáveis de qualidade e cor de fundo.",
"shortDescription": "Converta imagens para o formato WebP com controle de qualidade.",
"title": "Converter imagens para WebP",
"failedToConvert": "A conversão da(s) imagem(ns) para WebP falhou. Por favor, tente novamente.",
"inputTitle": "Imagens de entrada",
"resultTitle": "Imagens WEBP",
"options": {
"title": "Configurações de conversão",
"quality": "Qualidade de conversão:",
"qualityDescription": "Valores mais altos = melhor qualidade, tamanho de arquivo maior",
"colorDescription": "Cor de fundo (para imagens transparentes)"
}
}
}
+5
View File
@@ -116,5 +116,10 @@
"longDescription": "Esta ferramenta transpõe valores separados por vírgula (CSV). Ela trata o CSV como uma matriz de dados e inverte todos os elementos na diagonal principal. A saída contém os mesmos dados CSV que a entrada, mas agora todas as linhas se tornaram colunas e todas as colunas se tornaram linhas. Após a transposição, o arquivo CSV terá dimensões opostas. Por exemplo, se o arquivo de entrada tiver 4 colunas e 3 linhas, o arquivo de saída terá 3 colunas e 4 linhas. Durante a conversão, o programa também limpa os dados de linhas desnecessárias e corrige dados incompletos. Especificamente, a ferramenta exclui automaticamente todos os registros e comentários vazios que começam com um caractere específico, que você pode definir na opção. Além disso, nos casos em que os dados CSV são corrompidos ou perdidos, o utilitário completa o arquivo com campos vazios ou campos personalizados que podem ser especificados nas opções. CSV-abulous!",
"shortDescription": "Transponha rapidamente um arquivo CSV.",
"title": "Transpor CSV"
},
"tsvToJson": {
"description": "Converta dados TSV (Valores Separados por Tabulação) para o formato JSON. Transforme dados tabulares em objetos JSON estruturados.",
"shortDescription": "Converter TSV para o formato JSON",
"title": "TSV para JSON"
}
}
-5
View File
@@ -46,11 +46,6 @@
"shortDescription": "Converter objetos em string JSON",
"title": "Stringify JSON"
},
"tsvToJson": {
"description": "Converta dados TSV (Valores Separados por Tabulação) para o formato JSON. Transforme dados tabulares em objetos JSON estruturados.",
"shortDescription": "Converter TSV para o formato JSON",
"title": "TSV para JSON"
},
"validateJson": {
"description": "Verifique se JSON é válido e bem formado.",
"inputTitle": "Entrada JSON",
+4 -4
View File
@@ -86,8 +86,8 @@
"longDescription": "Altere a orientação das páginas do PDF girando-as 90, 180 ou 270 graus. Útil para corrigir documentos digitalizados incorretamente ou preparar PDFs para impressão.",
"pageRangesDescription": "Insira números de página ou intervalos separados por vírgulas (por exemplo, 1,3,5-7)",
"pageRangesPlaceholder": "por exemplo, 1,5-8",
"pagesWillBeRotated": "{{count}} página{{count !== 1 ? 's' : ''}} será girado",
"pdfPageCount": "PDF tem {{count}} página{{count !== 1 ? 's' : ''}}",
"pagesWillBeRotated": "{{count}} página{{plural}} será girado",
"pdfPageCount": "PDF tem {{count}} página{{plural}}",
"resultTitle": "PDF girado",
"rotatingPages": "Páginas rotativas",
"rotationAngle": "Ângulo de rotação",
@@ -103,11 +103,11 @@
"description": "Extraia páginas específicas de um documento PDF.",
"extractingPages": "Extraindo páginas",
"inputTitle": "PDF de entrada",
"pageExtractionPreview": "{{count}} página{{count !== 1 ? 's' : ''}} será extraído",
"pageExtractionPreview": "{{count}} página{{ plural }} será extraído",
"pageRangesDescription": "Insira números de página ou intervalos separados por vírgulas (por exemplo, 1,3,5-7)",
"pageRangesPlaceholder": "por exemplo, 1,5-8",
"pageSelection": "Seleção de página",
"pdfPageCount": "PDF tem {{count}} página{{count !== 1 ? 's' : ''}}",
"pdfPageCount": "PDF tem {{count}} página{{plural}}",
"resultTitle": "PDF extraído",
"shortDescription": "Extrair páginas específicas de um arquivo PDF",
"title": "Dividir PDF",
+26
View File
@@ -0,0 +1,26 @@
{
"audioConverter": {
"title": "Аудиоконвертер",
"description": "Преобразуйте аудиофайлы между различными форматами.",
"shortDescription": "Преобразуйте аудиофайлы в различные форматы.",
"longDescription": "Этот инструмент позволяет конвертировать аудиофайлы из одного формата в другой, поддерживая широкий диапазон аудиоформатов для безперебойного преобразования.",
"outputFormat": "Формат вывода",
"outputFormatDescription": "Выберите желаемый формат выходного аудио",
"inputTitle": "Аудиовход",
"outputTitle": "Преобразованное аудио"
},
"convertToWebp": {
"description": "Преобразуйте различные форматы изображений в формат WebP с настраиваемыми параметрами качества и цвета фона.",
"shortDescription": "Конвертируйте изображения в формат WebP с контролем качества.",
"title": "Преобразовать изображения в WebP",
"failedToConvert": "Ошибка при преобразовании изображения (изображений) в WebP. Пожалуйста, попробуйте еще раз.",
"inputTitle": "Входные изображения",
"resultTitle": "Изображения WEBP",
"options": {
"title": "Параметры преобразования",
"quality": "Качество преобразования:",
"qualityDescription": "Более высокие значения = лучшее качество, больший размер файла",
"colorDescription": "Цвет фона (для прозрачных изображений)"
}
}
}
+5
View File
@@ -116,5 +116,10 @@
"longDescription": "Этот инструмент транспонирует данные, разделенные запятыми (CSV). Он обрабатывает CSV как матрицу данных и переворачивает все элементы по главной диагонали. Выходные данные содержат те же данные CSV, что и входные, но теперь все строки стали столбцами, а все столбцы — строками. После транспонирования CSV-файл будет иметь противоположные размеры. Например, если входной файл содержит 4 столбца и 3 строки, выходной файл будет содержать 3 столбца и 4 строки. Во время преобразования программа также очищает данные от ненужных строк и исправляет неполные данные. В частности, инструмент автоматически удаляет все пустые записи и комментарии, начинающиеся с определенного символа, который можно задать в настройках. Кроме того, в случае повреждения или потери данных CSV-файла утилита дополняет файл пустыми полями или пользовательскими полями, которые можно указать в настройках. CSV-abulous!",
"shortDescription": "Быстро транспонируйте CSV-файл.",
"title": "Транспонировать CSV"
},
"tsvToJson": {
"description": "Преобразуйте данные TSV (значения, разделенные табуляцией) в формат JSON. Преобразуйте табличные данные в структурированные объекты JSON.",
"shortDescription": "Конвертировать формат TSV в JSON",
"title": "TSV в JSON"
}
}
-5
View File
@@ -46,11 +46,6 @@
"shortDescription": "Преобразование объектов в строку JSON",
"title": "Строкификация JSON"
},
"tsvToJson": {
"description": "Преобразуйте данные TSV (значения, разделенные табуляцией) в формат JSON. Преобразуйте табличные данные в структурированные объекты JSON.",
"shortDescription": "Конвертировать формат TSV в JSON",
"title": "TSV в JSON"
},
"validateJson": {
"description": "Проверьте корректность и правильность формата JSON.",
"inputTitle": "Входной JSON",
+4 -4
View File
@@ -86,8 +86,8 @@
"longDescription": "Измените ориентацию страниц PDF-файла, повернув их на 90, 180 или 270 градусов. Это полезно для исправления неправильно отсканированных документов или подготовки PDF-файлов к печати.",
"pageRangesDescription": "Введите номера страниц или диапазоны, разделенные запятыми (например, 1,3,5-7)",
"pageRangesPlaceholder": "например, 1,5-8",
"pagesWillBeRotated": "{{count}} страница{{count !== 1 ? 's' : ''}} будет вращаться",
"pdfPageCount": "PDF имеет {{count}} страница{{count !== 1 ? 's' : ''}}",
"pagesWillBeRotated": "{{count}} страница будет вращаться",
"pdfPageCount": "PDF имеет {{count}} страница",
"resultTitle": "Повернутый PDF-файл",
"rotatingPages": "Ротация страниц",
"rotationAngle": "Угол поворота",
@@ -103,11 +103,11 @@
"description": "Извлечение определенных страниц из PDF-документа.",
"extractingPages": "Извлечение страниц",
"inputTitle": "Входной PDF-файл",
"pageExtractionPreview": "{{count}} страница{{count !== 1 ? 's' : ''}} будут извлечены",
"pageExtractionPreview": "{{count}} страница будут извлечены",
"pageRangesDescription": "Введите номера страниц или диапазоны, разделенные запятыми (например, 1,3,5-7)",
"pageRangesPlaceholder": "например, 1,5-8",
"pageSelection": "Выбор страницы",
"pdfPageCount": "PDF имеет {{count}} страница{{count !== 1 ? 's' : ''}}",
"pdfPageCount": "PDF имеет {{count}} страница",
"resultTitle": "Извлеченный PDF-файл",
"shortDescription": "Извлечь определенные страницы из PDF-файла",
"title": "Разделить PDF",
+26
View File
@@ -0,0 +1,26 @@
{
"audioConverter": {
"title": "音频转换器",
"description": "在不同格式之间转换音频文件。",
"shortDescription": "将音频文件转换为各种格式。",
"longDescription": "此工具允许您将音频文件从一种格式转换为另一种格式,支持多种音频格式的无缝转换。",
"outputFormat": "输出格式",
"outputFormatDescription": "选择所需的输出音频格式",
"inputTitle": "音频输入",
"outputTitle": "转换后的音频"
},
"convertToWebp": {
"description": "使用可自定义的质量和背景颜色设置将各种图像格式转换为 WebP 格式。",
"shortDescription": "使用质量控制将图像转换为 WebP 格式。",
"title": "将图像转换为 WebP",
"failedToConvert": "将图像转换为 WebP 失败。请重试。",
"inputTitle": "输入图像",
"resultTitle": "WEBP 图像",
"options": {
"title": "转换设置",
"quality": "转换质量:",
"qualityDescription": "值越高 = 质量越好,文件大小越大",
"colorDescription": "背景颜色(用于透明图像)"
}
}
}
+5
View File
@@ -116,5 +116,10 @@
"longDescription": "此工具用于转置逗号分隔值 (CSV)。它将 CSV 视为数据矩阵,并将所有元素沿主对角线翻转。输出包含与输入相同的 CSV 数据,但现在所有行都变成了列,所有列都变成了行。转置后,CSV 文件的尺寸将相反。例如,如果输入文件有 4 列 3 行,则输出文件将有 3 列 4 行。在转换过程中,程序还会清除数据中不必要的行,并更正不完整的数据。具体来说,该工具会自动删除所有以特定字符开头的空记录和注释,您可以在选项中设置这些字符。此外,如果 CSV 数据损坏或丢失,该实用程序会使用空字段或自定义字段(可在选项中指定)来填充文件。CSV 真是太棒了!",
"shortDescription": "快速转置 CSV 文件。",
"title": "转置 CSV"
},
"tsvToJson": {
"description": "将 TSV(制表符分隔值)数据转换为 JSON 格式。将表格数据转换为结构化的 JSON 对象。",
"shortDescription": "将 TSV 转换为 JSON 格式",
"title": "TSV 到 JSON"
}
}
-5
View File
@@ -46,11 +46,6 @@
"shortDescription": "将对象转换为 JSON 字符串",
"title": "字符串化 JSON"
},
"tsvToJson": {
"description": "将 TSV(制表符分隔值)数据转换为 JSON 格式。将表格数据转换为结构化的 JSON 对象。",
"shortDescription": "将 TSV 转换为 JSON 格式",
"title": "TSV 到 JSON"
},
"validateJson": {
"description": "检查 JSON 是否有效且格式正确。",
"inputTitle": "输入 JSON",
+4 -4
View File
@@ -86,8 +86,8 @@
"longDescription": "通过将 PDF 页面旋转 90 度、180 度或 270 度来更改其方向。这对于修复扫描错误的文档或准备打印 PDF 非常有用。",
"pageRangesDescription": "输入页码或范围,以逗号分隔(例如,1,3,5-7)",
"pageRangesPlaceholder": "例如,1.5-8",
"pagesWillBeRotated": "{{count}} 页{{count !== 1 ? 's' : ''}} 将会轮换",
"pdfPageCount": "PDF 有 {{count}} 页{{count !== 1 ? 's' : ''}}",
"pagesWillBeRotated": "{{count}} 页 将会轮换",
"pdfPageCount": "PDF 有 {{count}} 页",
"resultTitle": "旋转的 PDF",
"rotatingPages": "旋转页面",
"rotationAngle": "旋转角度",
@@ -103,11 +103,11 @@
"description": "从 PDF 文档中提取特定页面。",
"extractingPages": "提取页面",
"inputTitle": "输入 PDF",
"pageExtractionPreview": "{{count}} 页{{count !== 1 ? 's' : ''}} 将被提取",
"pageExtractionPreview": "{{count}} 页 将被提取",
"pageRangesDescription": "输入页码或范围,以逗号分隔(例如,1,3,5-7)",
"pageRangesPlaceholder": "例如,1.5-8",
"pageSelection": "页面选择",
"pdfPageCount": "PDF 有 {{count}} 页{{count !== 1 ? 's' : ''}}",
"pdfPageCount": "PDF 有 {{count}} 页",
"resultTitle": "提取的 PDF",
"shortDescription": "从 PDF 文件中提取特定页面",
"title": "拆分 PDF",
+16
View File
@@ -307,5 +307,21 @@
"shortDescription": "快速对字符串进行 URL 转义。",
"title": "字符串 URL 编码器"
}
},
"unicode": {
"title": "Unicode 编码器 / 解码器",
"inputTitle": "输入",
"resultTitle": "处理结果",
"optionsTitle": "模式",
"caseOptionsTitle": "大小写选项",
"encode": "编码",
"decode": "解码",
"uppercase": "大写十六进制",
"description": "将文本转换为 Unicode 转义序列,或将其解码回可读文本。",
"shortDescription": "使用 Unicode 转义序列对文本进行编码或解码。",
"toolInfo": {
"title": "Unicode 编码器 / 解码器",
"description": "此工具可让您将纯文本转换为 Unicode 转义序列(例如,\\uXXXX),以及将 Unicode 转义序列解码回标准文本。您还可以选择编码时十六进制输出以大写或小写格式显示。"
}
}
}
+33
View File
@@ -113,5 +113,38 @@
"zeroPaddingDescription": "使所有时间部分始终为两位数宽度。",
"zeroPrintDescription": "将删除的部分显示为零值“00”。",
"zeroPrintTruncatedParts": "零打印截断部件"
},
"convertTimeToDecimal": {
"title": "把时间转换至十进制格式",
"description": "将格式化的时间长度(HH:MM:SS)转换为小数小时值。",
"shortDescription": "把时段转化为小数小时值",
"longDescription": "将格式化的时间字符串(HH:MM:SS 或 HH:MM)转换为其等效的小数小时值。小时可以是任意正数,而分钟和秒接受 0-59 之间的值,可以是一位或两位数字(例如,'12:5' 或 '12:05')。此函数解析小时、分钟和秒,然后将总时长计算为单一小数值。它适用于生产力追踪、薪资计算、按时计费、数据分析,或任何需要将人类可读时间转换为易于求和、比较或处理的数值格式的工作流程。例如,'03:26:00' 转换为 3.43 小时,'12:5' 转换为 12.08 小时。"
},
"discordTimestamp": {
"name": "Discord 时间戳生成器",
"description": "将日期和时间转换为 Discord 的特殊时间戳格式,自动适应每位用户的本地时区和语言",
"shortDescription": "从日期生成 Discord 时间戳",
"inputTitle": "输入日期时间",
"outputTitle": "Discord 时间戳",
"formats": {
"title": "时间戳格式",
"description": "选择时间戳在 Discord 中的显示方式 — 悬停在每个选项上可查看示例",
"short_time": "短时间(例:14:30)",
"long_time": "长时间(例:14:30:00)",
"short_date": "短日期(例:01/01/2025)",
"long_date": "长日期(例:2025年1月1日)",
"short_datetime": "短日期和时间(例:2025年1月1日 14:30)",
"long_datetime": "长日期和时间(例:2025年1月1日 星期一 14:30)",
"relative": "相对时间(例:2小时前)"
},
"utc": {
"title": "输入选项",
"label": "强制使用 UTC",
"description": "将输入的日期时间视为 UTC。禁用则使用您的本地时区"
},
"toolInfo": {
"title": "什么是 Discord 时间戳?",
"description": "Discord 时间戳是一种动态显示日期和时间的特殊语法。它不显示固定时间,而是自动转换为每位查看者的本地时区和语言 — 让每个人都能看到适合其所在位置的正确时间。只需每行输入一个日期时间,选择格式,然后将结果直接粘贴到 Discord 中。该工具独立处理每一行 — 空行在输出中保留,无效行以 ❌ 标记,有效的日期时间即时转换。启用 UTC 模式可确保结果不受本地时区影响。"
}
}
}
+5
View File
@@ -22,6 +22,10 @@
"description": "处理音频的工具——从视频中提取音频、调整音频速度、合并多个音频文件等等。",
"title": "音频工具"
},
"converters": {
"description": "用于在不同格式之间转换数据的工具 — 转换图片、音频、视频、文本等。",
"title": "转换工具"
},
"csv": {
"description": "处理 CSV 文件的工具 - 将 CSV 转换为不同的格式、操作 CSV 数据、验证 CSV 结构以及有效地处理 CSV 文件。",
"title": "CSV 工具"
@@ -73,6 +77,7 @@
"title": "XML 工具"
}
},
"csv": {
"findIncompleteCsvRecords": {
"description": "只需将您的 CSV 文件上传到下方表单,此工具就会自动检查所有行或列是否均缺失值。在工具选项中,您可以调整输入文件格式(指定分隔符、引号和注释符)。此外,您还可以启用空值检查、跳过空行,以及设置输出中错误消息的数量限制。",
+2
View File
@@ -12,6 +12,7 @@ import pdf from '../../public/locales/en/pdf.json';
import audio from '../../public/locales/en/audio.json';
import xml from '../../public/locales/en/xml.json';
import image from '../../public/locales/en/image.json';
import converters from '../../public/locales/en/converters.json';
declare module 'i18next' {
interface CustomTypeOptions {
@@ -28,6 +29,7 @@ declare module 'i18next' {
audio: typeof audio;
xml: typeof xml;
image: typeof image;
converters: typeof converters;
};
}
}
+2
View File
@@ -150,6 +150,8 @@ export default function Hero() {
sx={{ mb: 2 }}
autoHighlight
options={filteredTools}
// Disable default MUI filtering since we already apply custom filterTools
filterOptions={(options) => options}
groupBy={(option) => option.type}
renderGroup={(params) => {
return (
@@ -0,0 +1,207 @@
import React, { useRef } from 'react';
import { Box } from '@mui/material';
import Typography from '@mui/material/Typography';
import InputHeader from '../InputHeader';
import InputFooter from './InputFooter';
import ImageIcon from '@mui/icons-material/Image';
import { useTranslation } from 'react-i18next';
import { heicTo, isHeic } from 'heic-to';
interface MultiImageInputComponentProps {
accept: string[];
title?: string;
type: 'image';
value: MultiImageInput[];
onChange: (files: MultiImageInput[]) => void;
}
export interface MultiImageInput {
file: File;
order: number;
preview?: string;
}
export default function ToolMultiImageInput({
value,
onChange,
accept,
title,
type
}: MultiImageInputComponentProps) {
const { t } = useTranslation();
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileChange = async (
event: React.ChangeEvent<HTMLInputElement>
) => {
const files = event.target.files;
if (!files) return;
const newFiles: MultiImageInput[] = await Promise.all(
Array.from(files).map(async (file, index) => {
let processedFile = file;
try {
if (await isHeic(file)) {
const convertedBlob = await heicTo({
blob: file,
type: 'image/jpeg'
});
processedFile = new File(
[convertedBlob],
file.name.replace(/\.[^/.]+$/, '') + '.jpg',
{ type: 'image/jpeg' }
);
}
} catch (err) {
console.warn('HEIC conversion failed:', file.name, err);
}
return {
file: processedFile,
order: value.length + index,
preview: URL.createObjectURL(processedFile)
};
})
);
onChange([...value, ...newFiles]);
event.target.value = '';
};
const handleImportClick = () => {
fileInputRef.current?.click();
};
function handleClear() {
value.forEach((f) => {
if (f.preview) URL.revokeObjectURL(f.preview);
});
onChange([]);
}
function handleRemove(index: number) {
const fileToRemove = value[index];
if (fileToRemove?.preview) {
URL.revokeObjectURL(fileToRemove.preview);
}
const updatedFiles = value.filter((_, i) => i !== index);
onChange(updatedFiles);
}
function fileNameTruncate(fileName: string) {
const maxLength = 10;
return fileName.length > maxLength
? fileName.slice(0, maxLength) + '...'
: fileName;
}
return (
<Box>
<InputHeader
title={
title ||
t('toolMultipleImageInput.inputTitle', {
type: type.charAt(0).toUpperCase() + type.slice(1)
})
}
/>
<Box
sx={{
width: '100%',
height: '300px',
border: value?.length ? 0 : 1,
borderRadius: 2,
boxShadow: '5',
bgcolor: 'background.paper',
position: 'relative'
}}
>
<Box
width="100%"
height="100%"
sx={{
overflow: 'auto',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexWrap: 'wrap',
position: 'relative'
}}
>
{value?.length ? (
value.map((file, index) => (
<Box
key={index}
sx={{
margin: 1,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'space-between',
width: '120px',
border: 1,
borderRadius: 1,
padding: 1,
position: 'relative'
}}
>
{file.preview ? (
<Box
component="img"
src={file.preview}
alt={file.file.name}
sx={{
width: '100%',
height: '80px',
objectFit: 'cover',
borderRadius: 1
}}
/>
) : (
<ImageIcon sx={{ fontSize: 40 }} />
)}
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
mt: 0.5
}}
>
<Typography variant="caption">
{fileNameTruncate(file.file.name)}
</Typography>
<Box
sx={{ cursor: 'pointer' }}
onClick={() => handleRemove(index)}
>
✖
</Box>
</Box>
</Box>
))
) : (
<Typography variant="body2" color="text.secondary">
{t('toolMultipleImageInput.noFilesSelected')}
</Typography>
)}
</Box>
</Box>
<InputFooter handleImport={handleImportClick} handleClear={handleClear} />
<input
ref={fileInputRef}
style={{ display: 'none' }}
type="file"
accept={accept.join(',')}
onChange={handleFileChange}
multiple
/>
</Box>
);
}
+1 -1
View File
@@ -19,7 +19,7 @@ const CheckboxWithDesc = ({
};
return (
<Box>
<Box mb={3}>
<FormControlLabel
control={
<Checkbox
+2 -2
View File
@@ -27,7 +27,7 @@ const ColorSelector: React.FC<ColorSelectorProps & TextFieldProps> = ({
};
return (
<Box mb={1}>
<Box mb={3}>
<Stack direction={'row'}>
<TextField
sx={{ backgroundColor: 'background.paper' }}
@@ -46,7 +46,7 @@ const ColorSelector: React.FC<ColorSelectorProps & TextFieldProps> = ({
onChange={handleColorChange}
/>
</Stack>
<Typography fontSize={globalDescriptionFontSize}>
<Typography fontSize={globalDescriptionFontSize} mt={1}>
{description}
</Typography>
</Box>
@@ -22,7 +22,7 @@ const RadioWithTextField = ({
radioDescription?: string;
}) => {
return (
<Box>
<Box mb={3}>
<SimpleRadio
checked={checked}
onClick={onRadioClick}
+1 -1
View File
@@ -32,7 +32,7 @@ const SelectWithDesc = <T extends string | boolean>({
};
return (
<Box>
<Box mb={3}>
<Select value={selected} onChange={handleChange}>
{options.map((option) => (
<MenuItem key={option.label} value={option.value.toString()}>
+1 -1
View File
@@ -16,7 +16,7 @@ const TextFieldWithDesc = ({
...props
}: TextFieldProps & OwnProps) => {
return (
<Box>
<Box mb={3}>
<TextField
placeholder={placeholder}
sx={{ backgroundColor: 'background.paper' }}
+2 -1
View File
@@ -15,7 +15,8 @@ export const validNamespaces = [
'audio',
'xml',
'translation',
'image'
'image',
'converters'
] as const satisfies readonly Namespace[];
export type I18nNamespaces = (typeof validNamespaces)[number];
@@ -14,11 +14,8 @@ const initialValues: InitialValuesType = {
outputFormat: 'mp3'
};
export default function AudioConverter({
title,
longDescription
}: ToolComponentProps) {
const { t } = useTranslation('audio');
export default function AudioConverter({ title }: ToolComponentProps) {
const { t } = useTranslation('converters');
const [input, setInput] = useState<File | null>(null);
const [result, setResult] = useState<File | null>(null);
const [loading, setLoading] = useState(false);
@@ -47,12 +44,7 @@ export default function AudioConverter({
updateField
}) => [
{
id: 'output-format',
title: t('audioConverter.outputFormat', 'Output Format'),
description: t(
'audioConverter.outputFormatDescription',
'Select the desired output audio format'
),
title: t('audioConverter.outputFormat'),
component: (
<Box>
<SelectWithDesc
@@ -62,10 +54,7 @@ export default function AudioConverter({
label: value.toUpperCase(),
value: value as AudioFormat
}))}
description={t(
'audioConverter.outputFormatDescription',
'Select the desired output audio format'
)}
description={t('audioConverter.outputFormatDescription')}
/>
</Box>
)
@@ -80,13 +69,13 @@ export default function AudioConverter({
<ToolAudioInput
value={input}
onChange={setInput}
title={t('audioConverter.uploadAudio', 'Upload Your Audio File')}
title={t('audioConverter.inputTitle')}
/>
}
resultComponent={
<ToolFileResult
value={result}
title={t('audioConverter.outputTitle', 'Converted Audio')}
title={t('audioConverter.outputTitle')}
loading={loading}
/>
}
@@ -95,8 +84,8 @@ export default function AudioConverter({
setInput={setInput}
compute={compute}
toolInfo={{
title: `What is a ${title}?`,
description: longDescription
title: t('audioConverter.title'),
description: t('audioConverter.longDescription')
}}
/>
);
@@ -3,10 +3,10 @@ import { lazy } from 'react';
export const tool = defineTool('converters', {
i18n: {
name: 'translation:converters.audioconverter.title',
description: 'translation:converters.audioconverter.description',
shortDescription: 'translation:converters.audioconverter.shortDescription',
longDescription: 'translation:converters.audioconverter.longDescription'
name: 'converters:audioConverter.title',
description: 'converters:audioConverter.description',
shortDescription: 'converters:audioConverter.shortDescription',
longDescription: 'converters:audioConverter.longDescription'
},
path: 'audio-converter',
icon: 'mdi:music-note-outline',
@@ -0,0 +1,150 @@
import { Box, Slider, Typography } from '@mui/material';
import ColorSelector from 'components/options/ColorSelector';
import ToolFileResult from 'components/result/ToolFileResult';
import React, { useContext, useState, useCallback } from 'react';
import * as Yup from 'yup';
import ToolContent from '@components/ToolContent';
import { ToolComponentProps } from '@tools/defineTool';
import InitialValuesType from './types';
import ToolMultipleImageInput, {
MultiImageInput
} from '@components/input/ToolMultipleImageInput';
import ToolMultiFileResult from '@components/result/ToolMultiFileResult';
import { CustomSnackBarContext } from '../../../../contexts/CustomSnackBarContext';
import { convertToWebp } from './service';
import { useTranslation } from 'react-i18next';
import debounce from 'lodash/debounce';
const initialValues: InitialValuesType = {
backgroundColor: '#ffffff',
quality: 100
};
const validationSchema = Yup.object({
quality: Yup.number().min(1).max(100).required('Quality is required'),
backgroundColor: Yup.string().required('Background color is required'),
maxFileSizeInMb: Yup.number()
.min(1)
.max(100)
.required('Maximum file size is required')
});
export default function ConvertToWebp({ title }: ToolComponentProps) {
const { t } = useTranslation('converters');
const [input, setInput] = useState<MultiImageInput[]>([]);
const [result, setResult] = useState<File[]>([]);
const [zipFile, setZipFile] = useState<File | null>(null);
const [isProcessing, setIsProcessing] = useState<boolean>(false);
const { showSnackBar } = useContext(CustomSnackBarContext);
const compute = async (
optionsValues: typeof initialValues,
input: MultiImageInput[]
) => {
if (!input || input.length === 0) return;
// Running the Service
try {
setIsProcessing(true);
const output = await convertToWebp(
input.map((img) => img.file),
optionsValues.quality,
optionsValues.backgroundColor
);
if (!output) {
showSnackBar(t('convertToWebp.failedToConvert'), 'error');
return;
} else {
setResult(output.convertedFiles);
setZipFile(output.zipFile);
}
} catch (error) {
showSnackBar(`Error converting files: ${error}`, 'error');
setZipFile(null);
} finally {
setIsProcessing(false);
}
console.log('Images are processed');
};
const debouncedCompute = useCallback(debounce(compute, 1000), []);
return (
<ToolContent
title={title}
input={input}
inputComponent={
<ToolMultipleImageInput
value={input}
type={'image'}
onChange={setInput}
accept={['image/*']}
title={t('convertToWebp.inputTitle')}
/>
}
resultComponent={
zipFile ? (
<ToolMultiFileResult
title={t('convertToWebp.resultTitle')}
value={result}
zipFile={zipFile}
loading={isProcessing}
/>
) : (
<ToolFileResult
title={t('convertToWebp.resultTitle')}
value={result[0] ?? null}
extension={'webp'}
loading={isProcessing}
/>
)
}
initialValues={initialValues}
validationSchema={validationSchema}
getGroups={({ values, updateField }) => [
{
title: t('convertToWebp.options.title'),
component: (
<Box>
<Box mb={3}>
<Typography variant="body2" color="text.secondary" gutterBottom>
{t('convertToWebp.options.quality')} {values.quality}%
</Typography>
<Slider
value={values.quality}
onChange={(_, value) =>
updateField(
'quality',
Array.isArray(value) ? value[0] : value
)
}
min={1}
max={100}
step={1}
valueLabelDisplay="auto"
valueLabelFormat={(value) => `${value}%`}
sx={{ mt: 1 }}
/>
<Typography variant="caption" color="text.secondary">
{t('convertToWebp.options.qualityDescription')}
</Typography>
</Box>
<ColorSelector
value={values.backgroundColor}
onColorChange={(val) => updateField('backgroundColor', val)}
description={t('convertToWebp.options.colorDescription')}
inputProps={{ 'data-testid': 'background-color-input' }}
/>
</Box>
)
}
]}
compute={debouncedCompute}
setInput={setInput}
/>
);
}
@@ -0,0 +1,16 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
export const tool = defineTool('converters', {
i18n: {
name: 'converters:convertToWebp.title',
description: 'converters:convertToWebp.description',
shortDescription: 'converters:convertToWebp.shortDescription'
},
path: 'convert-to-webp',
icon: 'iconoir:webp-format',
keywords: ['convert', 'jpg', 'jpeg', 'png', 'webp', 'compression'],
component: lazy(() => import('./index'))
});
@@ -0,0 +1,105 @@
import JSZip from 'jszip';
export const convertToWebp = async (
files: File[],
quality: number,
backgroundColor: string
): Promise<{ convertedFiles: File[]; zipFile: File | null } | null> => {
try {
// Map Out the files
const output = await Promise.all(
files.map(async (file) => {
try {
// Process Images One by One
const processedImage = await processImage(
file,
quality,
backgroundColor
);
return processedImage;
} catch (error) {
console.error(`Error converting ${file.name} to WEBP:`, error);
return null;
}
})
);
// Filter Out The Valid Files
const convertedFiles = output.filter((f): f is File => f !== null);
// Checking the Need to Convert
if (convertedFiles.length === 0) return null;
if (convertedFiles.length === 1) return { convertedFiles, zipFile: null };
// Converting Into Zip File
const zip = new JSZip();
convertedFiles.forEach((file) => zip.file(file.name, file));
const zipBlob = await zip.generateAsync({ type: 'blob' });
const zipFile = new File([zipBlob], 'compressed-images.zip', {
type: 'application/zip'
});
if (convertedFiles) {
return { convertedFiles, zipFile };
} else {
return null;
}
} catch (error) {
throw new Error(error as string);
}
};
// Process and Return An Image
async function processImage(
file: File,
quality: number,
backgroundColor: string
): Promise<File | null> {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (ctx == null) return null;
const img = new Image();
img.src = URL.createObjectURL(file);
try {
await img.decode();
canvas.width = img.width;
canvas.height = img.height;
// Fill background with selected color (important for transparency)
let bgColor: [number, number, number];
try {
//@ts-ignore
bgColor = Color(backgroundColor).rgb().array();
} catch (err) {
bgColor = [255, 255, 255]; // Default to white
}
ctx.fillStyle = `rgb(${bgColor[0]}, ${bgColor[1]}, ${bgColor[2]})`;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw the image on top
ctx.drawImage(img, 0, 0);
const fileName = file.name.replace(/\.[^/.]+$/, '') + '.webp';
const blob: Blob = await new Promise((resolve, reject) => {
canvas.toBlob(
(b) => (b ? resolve(b) : reject('Canvas toBlob returned null')),
'image/webp',
quality / 100
);
});
return new File([blob], fileName, { type: 'image/webp' });
} catch (error) {
console.error(`Error converting ${file.name} to WEBP:`, error);
return null;
} finally {
URL.revokeObjectURL(img.src);
}
}
@@ -0,0 +1,4 @@
export default interface InitialValuesType {
backgroundColor: string;
quality: number;
}
+5 -1
View File
@@ -1,3 +1,7 @@
import { tool as convertersAudioConverter } from './audio-converter/meta';
import { tool as convertersWebpConverter } from './convert-to-webp/meta';
export const convertersTools = [convertersAudioConverter];
export const convertersTools = [
convertersAudioConverter,
convertersWebpConverter
];
+1 -1
View File
@@ -11,6 +11,6 @@ export const tool = defineTool('csv', {
path: 'csv-to-xml',
icon: 'mdi-light:xml',
keywords: ['csv', 'xml', 'convert', 'transform', 'parse'],
keywords: ['convert', 'transform', 'parse'],
component: lazy(() => import('./index'))
});
+1 -1
View File
@@ -11,6 +11,6 @@ export const tool = defineTool('csv', {
path: 'csv-to-yaml',
icon: 'nonicons:yaml-16',
keywords: ['csv', 'to', 'yaml'],
keywords: ['comma', 'separated', 'values', 'transform', 'markup', 'convert'],
component: lazy(() => import('./index'))
});
+3 -1
View File
@@ -7,6 +7,7 @@ import { tool as csvToJson } from './csv-to-json/meta';
import { tool as csvToXml } from './csv-to-xml/meta';
import { tool as csvToRowsColumns } from './csv-rows-to-columns/meta';
import { tool as csvToTsv } from './csv-to-tsv/meta';
import { tool as tsvToJson } from './tsv-to-json/meta';
import { tool as swapCsvColumns } from './swap-csv-columns/meta';
export const csvTools = [
@@ -19,5 +20,6 @@ export const csvTools = [
ChangeCsvDelimiter,
findIncompleteCsvRecords,
transposeCsv,
insertCsvColumns
insertCsvColumns,
tsvToJson
];
@@ -1,15 +1,15 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
export const tool = defineTool('json', {
export const tool = defineTool('csv', {
path: 'tsv-to-json',
icon: 'material-symbols:code',
keywords: ['tsv', 'json', 'convert', 'tabular'],
component: lazy(() => import('./index')),
i18n: {
name: 'json:tsvToJson.title',
description: 'json:tsvToJson.description',
shortDescription: 'json:tsvToJson.shortDescription'
name: 'csv:tsvToJson.title',
description: 'csv:tsvToJson.description',
shortDescription: 'csv:tsvToJson.shortDescription'
}
});
@@ -1,6 +1,6 @@
import { InitialValuesType } from './types';
import { beautifyJson } from '../prettify/service';
import { minifyJson } from '../minify/service';
import { beautifyJson } from '../../json/prettify/service';
import { minifyJson } from '../../json/minify/service';
export function convertTsvToJson(
input: string,
@@ -11,6 +11,6 @@ export const tool = defineTool('image-generic', {
path: 'change-colors',
icon: 'cil:color-fill',
keywords: ['change', 'colors', 'in', 'png', 'image', 'jpg'],
keywords: ['png', 'jpg', 'replace', 'swap'],
component: lazy(() => import('./index'))
});
@@ -1,11 +1,14 @@
import React, { useContext, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { InitialValuesType } from './types';
import { compressImage } from './service';
import { compressImages } from './service';
import ToolContent from '@components/ToolContent';
import ToolImageInput from '@components/input/ToolImageInput';
import ToolMultipleImageInput, {
MultiImageInput
} from '@components/input/ToolMultipleImageInput';
import { ToolComponentProps } from '@tools/defineTool';
import ToolFileResult from '@components/result/ToolFileResult';
import ToolMultiFileResult from '@components/result/ToolMultiFileResult';
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
import { Box } from '@mui/material';
import Typography from '@mui/material/Typography';
@@ -19,28 +22,42 @@ const initialValues: InitialValuesType = {
export default function CompressImage({ title }: ToolComponentProps) {
const { t } = useTranslation('image');
const [input, setInput] = useState<File | null>(null);
const [result, setResult] = useState<File | null>(null);
const [input, setInput] = useState<MultiImageInput[]>([]);
const [results, setResults] = useState<File[]>([]);
const [zipFile, setZipFile] = useState<File | null>(null);
const [isProcessing, setIsProcessing] = useState<boolean>(false);
const [originalSize, setOriginalSize] = useState<number | null>(null); // Store original file size
const [compressedSize, setCompressedSize] = useState<number | null>(null); // Store compressed file size
const { showSnackBar } = useContext(CustomSnackBarContext);
const compute = async (values: InitialValuesType, input: File | null) => {
if (!input) return;
const compute = async (
values: InitialValuesType,
input: MultiImageInput[]
) => {
if (!input.length) return;
setOriginalSize(input.reduce((acc, img) => acc + img.file.size, 0));
setOriginalSize(input.size);
try {
setIsProcessing(true);
const compressed = await compressImage(input, values);
const output = await compressImages(
input.map((img) => img.file),
values
);
if (compressed) {
setResult(compressed);
setCompressedSize(compressed.size);
} else {
if (!output) {
showSnackBar(t('compress.failedToCompress'), 'error');
return;
}
if (output.results.length < input.length) {
showSnackBar(t('compress.failedToCompress'), 'error');
}
setResults(output.results);
setZipFile(output.zipFile);
setCompressedSize(output.results.reduce((acc, f) => acc + f.size, 0));
} catch (err) {
console.error('Error in compression:', err);
} finally {
@@ -53,19 +70,29 @@ export default function CompressImage({ title }: ToolComponentProps) {
title={title}
input={input}
inputComponent={
<ToolImageInput
<ToolMultipleImageInput
value={input}
type={'image'}
onChange={setInput}
accept={['image/*']}
title={t('compress.inputTitle')}
/>
}
resultComponent={
<ToolFileResult
title={t('compress.resultTitle')}
value={result}
loading={isProcessing}
/>
zipFile ? (
<ToolMultiFileResult
title={t('compress.resultTitle')}
value={results}
zipFile={zipFile}
loading={isProcessing}
/>
) : (
<ToolFileResult
title={t('compress.resultTitle')}
value={results[0] ?? null}
extension={results[0]?.name.split('.').pop() || 'png'}
/>
)
}
initialValues={initialValues}
getGroups={({ values, updateField }) => [
@@ -1,10 +1,11 @@
import { InitialValuesType } from './types';
import imageCompression from 'browser-image-compression';
import JSZip from 'jszip';
export const compressImage = async (
file: File,
export const compressImages = async (
files: File[],
options: InitialValuesType
): Promise<File | null> => {
): Promise<{ results: File[]; zipFile: File | null } | null> => {
try {
const { maxFileSizeInMB, quality } = options;
@@ -16,15 +17,40 @@ export const compressImage = async (
initialQuality: quality / 100 // Convert percentage to decimal
};
// Compress the image
const compressedFile = await imageCompression(file, compressionOptions);
// Compress the given images
const compressed = await Promise.all(
files.map(async (file) => {
try {
const compressedFile = await imageCompression(
file,
compressionOptions
);
return new File([compressedFile], file.name, {
type: compressedFile.type
});
} catch (error) {
console.error(`Error compressing ${file.name}:`, error);
return null;
}
})
);
// Create a new file with the original name
return new File([compressedFile], file.name, {
type: compressedFile.type
const results = compressed.filter((f): f is File => f !== null);
if (results.length === 0) return null;
if (results.length === 1) return { results, zipFile: null };
const zip = new JSZip();
results.forEach((file) => zip.file(file.name, file));
const zipBlob = await zip.generateAsync({ type: 'blob' });
const zipFile = new File([zipBlob], 'compressed-images.zip', {
type: 'application/zip'
});
return { results, zipFile };
} catch (error) {
console.error('Error compressing image:', error);
console.error('Error compressing images:', error);
return null;
}
};
@@ -12,10 +12,8 @@ export const tool = defineTool('image-generic', {
icon: 'mdi:text-recognition', // Iconify icon as a string
keywords: [
'ocr',
'optical character recognition',
'image to text',
'extract text',
'extract',
'scan',
'tesseract',
'jpg',
+1
View File
@@ -10,6 +10,7 @@ import { tool as qrCodeGenerator } from './qr-code/meta';
import { tool as rotateImage } from './rotate/meta';
import { tool as convertToJpg } from './convert-to-jpg/meta';
import { tool as imageEditor } from './editor/meta';
export const imageGenericTools = [
imageEditor,
resizeImage,
+55 -60
View File
@@ -1,13 +1,15 @@
import React, { useCallback, useState } from 'react';
import { Box, MenuItem, TextField } from '@mui/material';
import { Box } from '@mui/material';
import * as Yup from 'yup';
import ToolContent from '@components/ToolContent';
import { ToolComponentProps } from '@tools/defineTool';
import { GetGroupsType } from '@components/options/ToolOptions';
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
import SelectWithDesc from '@components/options/SelectWithDesc';
import { InitialValuesType, QRCodeType, WifiEncryptionType } from './types';
import ColorSelector from '@components/options/ColorSelector';
import ToolFileResult from '@components/result/ToolFileResult';
import { useTranslation } from 'react-i18next';
import * as QRCode from 'qrcode';
import { debounce } from 'lodash';
@@ -166,6 +168,7 @@ const validationSchema = Yup.object().shape({
});
export default function QRCodeGenerator({ title }: ToolComponentProps) {
const { t } = useTranslation('image');
const [result, setResult] = useState<File | null>(null);
const getGroups: GetGroupsType<InitialValuesType> = ({
values,
@@ -176,24 +179,22 @@ export default function QRCodeGenerator({ title }: ToolComponentProps) {
title: 'QR Code Type',
component: (
<Box>
<TextField
select
fullWidth
value={values.qrCodeType}
onChange={(e) =>
updateField('qrCodeType', e.target.value as QRCodeType)
<SelectWithDesc
selected={values.qrCodeType}
onChange={(value) =>
updateField('qrCodeType', value as QRCodeType)
}
label="Select QR Code Type"
margin="normal"
>
<MenuItem value="URL">URL</MenuItem>
<MenuItem value="Text">Text</MenuItem>
<MenuItem value="Email">Email</MenuItem>
<MenuItem value="Phone">Phone</MenuItem>
<MenuItem value="SMS">SMS</MenuItem>
<MenuItem value="WiFi">WiFi</MenuItem>
<MenuItem value="vCard">vCard (Contact)</MenuItem>
</TextField>
options={[
{ label: 'URL', value: 'URL' },
{ label: 'Text', value: 'Text' },
{ label: 'Email', value: 'Email' },
{ label: 'Phone', value: 'Phone' },
{ label: 'SMS', value: 'SMS' },
{ label: 'WiFi', value: 'WiFi' },
{ label: 'vCard', value: 'vCard' }
]}
description={t('qrCode.options.qrType')}
/>
</Box>
)
},
@@ -204,7 +205,7 @@ export default function QRCodeGenerator({ title }: ToolComponentProps) {
<TextFieldWithDesc
value={values.size}
onOwnChange={(val) => updateField('size', val)}
description="Size in pixels (100-1000)"
description={t('qrCode.options.size')}
inputProps={{
type: 'number',
min: 100,
@@ -233,7 +234,7 @@ export default function QRCodeGenerator({ title }: ToolComponentProps) {
<TextFieldWithDesc
value={values.url}
onOwnChange={(val) => updateField('url', val)}
description="Enter the URL"
description={t('qrCode.options.url')}
inputProps={{
placeholder: 'https://example.com'
}}
@@ -244,11 +245,11 @@ export default function QRCodeGenerator({ title }: ToolComponentProps) {
<TextFieldWithDesc
value={values.text}
onOwnChange={(val) => updateField('text', val)}
description="Enter the text"
description={t('qrCode.options.text')}
multiline
rows={4}
inputProps={{
placeholder: 'Enter your text here'
placeholder: 'Lorem Ipsum'
}}
/>
)}
@@ -258,7 +259,7 @@ export default function QRCodeGenerator({ title }: ToolComponentProps) {
<TextFieldWithDesc
value={values.emailAddress}
onOwnChange={(val) => updateField('emailAddress', val)}
description="Email Address"
description={t('qrCode.options.email')}
inputProps={{
placeholder: 'example@example.com',
type: 'email'
@@ -267,7 +268,7 @@ export default function QRCodeGenerator({ title }: ToolComponentProps) {
<TextFieldWithDesc
value={values.emailSubject}
onOwnChange={(val) => updateField('emailSubject', val)}
description="Email Subject (optional)"
description={t('qrCode.options.emailSubject')}
inputProps={{
placeholder: 'Subject line'
}}
@@ -275,7 +276,7 @@ export default function QRCodeGenerator({ title }: ToolComponentProps) {
<TextFieldWithDesc
value={values.emailBody}
onOwnChange={(val) => updateField('emailBody', val)}
description="Email Body (optional)"
description={t('qrCode.options.emailBody')}
multiline
rows={4}
inputProps={{
@@ -289,7 +290,7 @@ export default function QRCodeGenerator({ title }: ToolComponentProps) {
<TextFieldWithDesc
value={values.phoneNumber}
onOwnChange={(val) => updateField('phoneNumber', val)}
description="Phone Number"
description={t('qrCode.options.phone')}
inputProps={{
placeholder: '+1234567890',
type: 'tel'
@@ -302,7 +303,7 @@ export default function QRCodeGenerator({ title }: ToolComponentProps) {
<TextFieldWithDesc
value={values.smsNumber}
onOwnChange={(val) => updateField('smsNumber', val)}
description="Phone Number"
description={t('qrCode.options.phone')}
inputProps={{
placeholder: '+1234567890',
type: 'tel'
@@ -311,11 +312,11 @@ export default function QRCodeGenerator({ title }: ToolComponentProps) {
<TextFieldWithDesc
value={values.smsMessage}
onOwnChange={(val) => updateField('smsMessage', val)}
description="Message (optional)"
description={t('qrCode.options.message')}
multiline
rows={4}
inputProps={{
placeholder: 'Your message here'
placeholder: 'Lorem Ipsum'
}}
/>
</>
@@ -326,37 +327,32 @@ export default function QRCodeGenerator({ title }: ToolComponentProps) {
<TextFieldWithDesc
value={values.wifiSsid}
onOwnChange={(val) => updateField('wifiSsid', val)}
description="Network Name (SSID)"
description={t('qrCode.options.ssid')}
inputProps={{
placeholder: 'Network name'
placeholder: 'WIFI name'
}}
/>
<TextFieldWithDesc
value={values.wifiPassword}
onOwnChange={(val) => updateField('wifiPassword', val)}
description="Password"
description={t('qrCode.options.password')}
inputProps={{
placeholder: 'Password',
placeholder: '******',
type: 'password'
}}
/>
<TextField
select
fullWidth
value={values.wifiEncryption}
onChange={(e) =>
updateField(
'wifiEncryption',
e.target.value as WifiEncryptionType
)
<SelectWithDesc
selected={values.wifiEncryption}
onChange={(value) =>
updateField('wifiEncryption', value as WifiEncryptionType)
}
label="Encryption Type"
margin="normal"
>
<MenuItem value="WPA">WPA</MenuItem>
<MenuItem value="WEP">WEP</MenuItem>
<MenuItem value="None">None</MenuItem>
</TextField>
options={[
{ label: 'WPA', value: 'WPA' },
{ label: 'WEP', value: 'WEP' },
{ label: 'None', value: 'None' }
]}
description={t('qrCode.options.encryptionType')}
/>
</>
)}
@@ -365,7 +361,7 @@ export default function QRCodeGenerator({ title }: ToolComponentProps) {
<TextFieldWithDesc
value={values.vCardName}
onOwnChange={(val) => updateField('vCardName', val)}
description="Full Name"
description={t('qrCode.options.fullName')}
inputProps={{
placeholder: 'John Doe'
}}
@@ -373,7 +369,7 @@ export default function QRCodeGenerator({ title }: ToolComponentProps) {
<TextFieldWithDesc
value={values.vCardEmail}
onOwnChange={(val) => updateField('vCardEmail', val)}
description="Email"
description={t('qrCode.options.email')}
inputProps={{
placeholder: 'john@example.com',
type: 'email'
@@ -382,7 +378,7 @@ export default function QRCodeGenerator({ title }: ToolComponentProps) {
<TextFieldWithDesc
value={values.vCardPhone}
onOwnChange={(val) => updateField('vCardPhone', val)}
description="Phone"
description={t('qrCode.options.phone')}
inputProps={{
placeholder: '+1234567890',
type: 'tel'
@@ -391,7 +387,7 @@ export default function QRCodeGenerator({ title }: ToolComponentProps) {
<TextFieldWithDesc
value={values.vCardAddress}
onOwnChange={(val) => updateField('vCardAddress', val)}
description="Address"
description={t('qrCode.options.address')}
inputProps={{
placeholder: '123 Main St, City, Country'
}}
@@ -399,7 +395,7 @@ export default function QRCodeGenerator({ title }: ToolComponentProps) {
<TextFieldWithDesc
value={values.vCardCompany}
onOwnChange={(val) => updateField('vCardCompany', val)}
description="Company (optional)"
description={t('qrCode.options.company')}
inputProps={{
placeholder: 'Company name'
}}
@@ -407,7 +403,7 @@ export default function QRCodeGenerator({ title }: ToolComponentProps) {
<TextFieldWithDesc
value={values.vCardTitle}
onOwnChange={(val) => updateField('vCardTitle', val)}
description="Job Title (optional)"
description={t('qrCode.options.job')}
inputProps={{
placeholder: 'Software Developer'
}}
@@ -415,7 +411,7 @@ export default function QRCodeGenerator({ title }: ToolComponentProps) {
<TextFieldWithDesc
value={values.vCardWebsite}
onOwnChange={(val) => updateField('vCardWebsite', val)}
description="Website (optional)"
description={t('qrCode.options.website')}
inputProps={{
placeholder: 'https://example.com'
}}
@@ -460,12 +456,11 @@ export default function QRCodeGenerator({ title }: ToolComponentProps) {
validationSchema={validationSchema}
compute={debouncedCompute}
resultComponent={
<ToolFileResult title={'Generated QR code'} value={result} />
<ToolFileResult title={t('qrCode.resultOutput')} value={result} />
}
toolInfo={{
title: 'QR Code Generator',
description:
'Generate QR codes for different data types: URL, Text, Email, Phone, SMS, WiFi, vCard, and more. Customize the size and colors to create the perfect QR code for your needs.'
title: t('qrCode.title'),
description: t('qrCode.description')
}}
/>
);
@@ -11,8 +11,9 @@ export const tool = defineTool('image-generic', {
path: 'qr-code',
icon: 'mdi:qrcode', // Iconify icon as a string
keywords: [
'qr code',
'qrcode',
'quick',
'read',
'generator',
'url',
'text',
@@ -21,7 +22,8 @@ export const tool = defineTool('image-generic', {
'sms',
'wifi',
'vcard',
'contact'
'contact',
'barcode'
],
component: lazy(() => import('./index'))
});
@@ -11,14 +11,6 @@ export const tool = defineTool('image-generic', {
path: 'remove-background',
icon: 'mdi:image-remove',
keywords: [
'remove',
'background',
'png',
'transparent',
'image',
'ai',
'jpg'
],
keywords: ['png', 'transparent', 'image', 'ai', 'jpg', 'backing', 'backdrop'],
component: lazy(() => import('./index'))
});
+56 -14
View File
@@ -1,15 +1,18 @@
import { Box } from '@mui/material';
import React, { useState } from 'react';
import * as Yup from 'yup';
import ToolImageInput from '@components/input/ToolImageInput';
import ToolFileResult from '@components/result/ToolFileResult';
import { GetGroupsType } from '@components/options/ToolOptions';
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
import ToolContent from '@components/ToolContent';
import { ToolComponentProps } from '@tools/defineTool';
import SimpleRadio from '@components/options/SimpleRadio';
import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
import { processImage } from './service';
import ToolMultipleImageInput, {
MultiImageInput
} from '@components/input/ToolMultipleImageInput';
import ToolMultiFileResult from '@components/result/ToolMultiFileResult';
import ToolFileResult from '@components/result/ToolFileResult';
import { resizeImages } from './service';
import { InitialValuesType } from './types';
import { useTranslation } from 'react-i18next';
@@ -47,12 +50,35 @@ const validationSchema = Yup.object({
export default function ResizeImage({ title }: ToolComponentProps) {
const { t } = useTranslation('image');
const [input, setInput] = useState<File | null>(null);
const [result, setResult] = useState<File | null>(null);
const [input, setInput] = useState<MultiImageInput[]>([]);
const [results, setResults] = useState<File[]>([]);
const [zipFile, setZipFile] = useState<File | null>(null);
const [isProcessing, setIsProcessing] = useState<boolean>(false);
const compute = async (optionsValues: InitialValuesType, input: any) => {
const compute = async (
optionsValues: InitialValuesType,
input: MultiImageInput[]
) => {
if (!input) return;
setResult(await processImage(input, optionsValues));
try {
setIsProcessing(true);
const output = await resizeImages(
input.map((img) => img.file),
optionsValues
);
if (!output) {
return;
}
setResults(output.results);
setZipFile(output.zipFile);
} catch (err) {
console.error('Error in resizing:', err);
} finally {
setIsProcessing(false);
}
};
const getGroups: GetGroupsType<InitialValuesType> = ({
@@ -171,19 +197,35 @@ export default function ResizeImage({ title }: ToolComponentProps) {
input={input}
validationSchema={validationSchema}
inputComponent={
<ToolImageInput
<ToolMultipleImageInput
value={input}
onChange={setInput}
accept={['image/jpeg', 'image/png', 'image/svg+xml', 'image/gif']}
type="image"
accept={[
'image/jpeg',
'image/png',
'image/svg+xml',
'image/gif',
'image/webp'
]}
title={t('resize.inputTitle')}
/>
}
resultComponent={
<ToolFileResult
title={t('resize.resultTitle')}
value={result}
extension={input?.name.split('.').pop() || 'png'}
/>
zipFile ? (
<ToolMultiFileResult
title={t('resize.resultTitle')}
value={results}
zipFile={zipFile}
loading={isProcessing}
/>
) : (
<ToolFileResult
title={t('resize.resultTitle')}
value={results[0] ?? null}
extension={results[0]?.name.split('.').pop() || 'png'}
/>
)
}
toolInfo={{
title: t('resize.toolInfo.title'),
@@ -1,8 +1,9 @@
import { InitialValuesType } from './types';
import { FFmpeg } from '@ffmpeg/ffmpeg';
import { fetchFile, toBlobURL } from '@ffmpeg/util';
import { fetchFile } from '@ffmpeg/util';
import JSZip from 'jszip';
export const processImage = async (
const processImage = async (
file: File,
options: InitialValuesType
): Promise<File | null> => {
@@ -216,3 +217,39 @@ export const processImage = async (
}, outputType);
});
};
export const resizeImages = async (
files: File[],
options: InitialValuesType
): Promise<{ results: File[]; zipFile: File | null } | null> => {
try {
const processed = await Promise.all(
files.map(async (file) => {
try {
return await processImage(file, options);
} catch (error) {
console.error(`Error processing ${file.name}:`, error);
return null;
}
})
);
const results = processed.filter((f): f is File => f !== null);
if (results.length === 0) return null;
if (results.length === 1) return { results, zipFile: null };
const zip = new JSZip();
results.forEach((file) => zip.file(file.name, file));
const zipBlob = await zip.generateAsync({ type: 'blob' });
const zipFile = new File([zipBlob], 'resized-images.zip', {
type: 'application/zip'
});
return { results, zipFile };
} catch (error) {
console.error('Error processing images:', error);
return null;
}
};
@@ -6,7 +6,6 @@ import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
import ToolFileResult from 'components/result/ToolFileResult';
import Color from 'color';
import React, { useState } from 'react';
import * as Yup from 'yup';
import { areColorsSimilar } from 'utils/color';
import ToolContent from '@components/ToolContent';
import { ToolComponentProps } from '@tools/defineTool';
@@ -16,9 +15,7 @@ const initialValues = {
color: 'white',
similarity: '10'
};
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
});
export default function ConvertJgpToPng({ title }: ToolComponentProps) {
const [input, setInput] = useState<File | null>(null);
const [result, setResult] = useState<File | null>(null);
+2 -2
View File
@@ -4,8 +4,8 @@ import { tool as jsonStringify } from './stringify/meta';
import { tool as validateJson } from './validateJson/meta';
import { tool as jsonToXml } from './json-to-xml/meta';
import { tool as escapeJson } from './escape-json/meta';
import { tool as tsvToJson } from './tsv-to-json/meta';
import { tool as jsonComparison } from './json-comparison/meta';
import { tool as jsonToCsv } from './json-to-csv/meta';
export const jsonTools = [
validateJson,
@@ -13,7 +13,7 @@ export const jsonTools = [
jsonMinify,
jsonStringify,
jsonToXml,
jsonToCsv,
escapeJson,
tsvToJson,
jsonComparison
];
+165
View File
@@ -0,0 +1,165 @@
import { useState } from 'react';
import ToolContent from '@components/ToolContent';
import ToolCodeInput from '@components/input/ToolCodeInput';
import ToolTextResult from '@components/result/ToolTextResult';
import { convertJsonToCsv } from './service';
import { CardExampleType } from '@components/examples/ToolExamples';
import { ToolComponentProps } from '@tools/defineTool';
import { Box } from '@mui/material';
import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
import SimpleRadio from '@components/options/SimpleRadio';
import { InitialValuesType } from './types';
import { useTranslation } from 'react-i18next';
const initialValues: InitialValuesType = {
delimiter: ',',
includeHeaders: true,
quoteStrings: 'auto'
};
const exampleCards: CardExampleType<InitialValuesType>[] = [
{
title: 'Array of objects',
description:
'Convert multiple JSON objects into CSV rows, one row per object.',
sampleText: `[
{ "name": "John Doe", "age": 25, "city": "New York" },
{ "name": "Jane Doe", "age": 30, "city": "Los Angeles" },
{ "name": "Bob Smith", "age": 22, "city": "Chicago" }
]`,
sampleResult: `name,age,city\nJohn Doe,25,New York\nJane Doe,30,Los Angeles\nBob Smith,22,Chicago`,
sampleOptions: {
...initialValues
}
},
{
title: 'Nested object (dot notation)',
description:
'Nested keys are flattened using dot notation (e.g. address.city).',
sampleText: `[
{
"name": "John Doe",
"age": 25,
"address": {
"street": "123 Main St",
"city": "New York",
"state": "NY",
"postalCode": "10001"
},
"hobbies": ["reading", "running"]
}
]`,
sampleResult: `name,age,address.street,address.city,address.state,address.postalCode,hobbies[0],hobbies[1]\nJohn Doe,25,123 Main St,New York,NY,10001,reading,running`,
sampleOptions: {
...initialValues
}
},
{
title: 'Sparse rows',
description:
'Missing keys are filled with empty values to keep columns aligned.',
sampleText: `[
{ "name": "Alice", "age": 30 },
{ "name": "Bob", "city": "Paris" },
{ "name": "Carol", "age": 25, "city": "Rome" }
]`,
sampleResult: `name,age,city\nAlice,30,\nBob,,Paris\nCarol,25,Rome`,
sampleOptions: {
...initialValues
}
}
];
export default function JsonToCsv({ title }: ToolComponentProps) {
const { t } = useTranslation('json');
const [input, setInput] = useState<string>('');
const [result, setResult] = useState<string>('');
const compute = (values: InitialValuesType, input: string) => {
if (input) {
try {
const csvResult = convertJsonToCsv(input, values);
setResult(csvResult);
} catch (error) {
setResult(
`Error: ${
error instanceof Error ? error.message : 'Invalid JSON format'
}`
);
}
}
};
return (
<ToolContent
title={title}
input={input}
setInput={setInput}
initialValues={initialValues}
compute={compute}
exampleCards={exampleCards}
inputComponent={
<ToolCodeInput
title={t('jsonToCsv.inputTitle')}
value={input}
onChange={setInput}
language="json"
/>
}
resultComponent={
<ToolTextResult
title={t('jsonToCsv.outputTitle')}
value={result}
extension={'csv'}
/>
}
getGroups={({ values, updateField }) => [
{
title: t('jsonToCsv.delimiterOption'),
component: (
<Box>
<TextFieldWithDesc
description={t('jsonToCsv.options.delimiter')}
value={values.delimiter}
onOwnChange={(val) => updateField('delimiter', val)}
/>
</Box>
)
},
{
title: t('jsonToCsv.quotingOption'),
component: (
<Box>
<SimpleRadio
checked={values.quoteStrings === 'auto'}
title={t('jsonToCsv.options.autoQuote.label')}
description={t('jsonToCsv.options.autoQuote.description')}
onClick={() => updateField('quoteStrings', 'auto')}
/>
<SimpleRadio
checked={values.quoteStrings === 'always'}
title={t('jsonToCsv.options.alwaysQuote.label')}
description={t('jsonToCsv.options.alwaysQuote.description')}
onClick={() => updateField('quoteStrings', 'always')}
/>
</Box>
)
},
{
title: t('jsonToCsv.headerOption'),
component: (
<Box>
<CheckboxWithDesc
checked={values.includeHeaders}
onChange={(value) => updateField('includeHeaders', value)}
title={t('jsonToCsv.options.header.label')}
description={t('jsonToCsv.options.header.description')}
/>
</Box>
)
}
]}
/>
);
}
+15
View File
@@ -0,0 +1,15 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
export const tool = defineTool('json', {
path: 'json-to-csv',
icon: 'material-symbols:code',
keywords: [],
component: lazy(() => import('./index')),
i18n: {
name: 'json:jsonToCsv.title',
description: 'json:jsonToCsv.description',
shortDescription: 'json:jsonToCsv.shortDescription',
userTypes: ['generalUsers', 'developers']
}
});
@@ -0,0 +1,172 @@
import { describe, it, expect } from 'vitest';
import { convertJsonToCsv } from './service';
const defaultOptions: Parameters<typeof convertJsonToCsv>[1] = {
delimiter: ',',
includeHeaders: true,
quoteStrings: 'auto'
};
describe('convertJsonToCsv', () => {
describe('basic conversion', () => {
it('converts a flat array of objects', () => {
const input = JSON.stringify([
{ name: 'Alice', age: 30 },
{ name: 'Bob', age: 25 }
]);
expect(convertJsonToCsv(input, defaultOptions)).toBe(
`name,age\r\nAlice,30\r\nBob,25`
);
});
it('converts a single object', () => {
const input = JSON.stringify({ name: 'Alice', age: 30 });
expect(convertJsonToCsv(input, defaultOptions)).toBe(
`name,age\r\nAlice,30`
);
});
it('excludes header row when includeHeaders is false', () => {
const input = JSON.stringify([{ name: 'Alice', age: 30 }]);
expect(
convertJsonToCsv(input, { ...defaultOptions, includeHeaders: false })
).toBe(`Alice,30`);
});
});
describe('flattening', () => {
it('flattens nested objects using dot notation', () => {
const input = JSON.stringify([
{ name: 'Alice', address: { city: 'Paris', zip: '75000' } }
]);
expect(convertJsonToCsv(input, defaultOptions)).toBe(
`name,address.city,address.zip\r\nAlice,Paris,75000`
);
});
it('flattens arrays using index notation', () => {
const input = JSON.stringify([
{ name: 'Alice', tags: ['admin', 'user'] }
]);
expect(convertJsonToCsv(input, defaultOptions)).toBe(
`name,tags[0],tags[1]\r\nAlice,admin,user`
);
});
it('flattens deeply nested structures', () => {
const input = JSON.stringify([
{ user: { address: { geo: { lat: 48.8566, lng: 2.3522 } } } }
]);
expect(convertJsonToCsv(input, defaultOptions)).toBe(
`user.address.geo.lat,user.address.geo.lng\r\n48.8566,2.3522`
);
});
});
describe('sparse rows', () => {
it('fills missing keys with empty values', () => {
const input = JSON.stringify([
{ name: 'Alice', age: 30 },
{ name: 'Bob', city: 'Paris' }
]);
expect(convertJsonToCsv(input, defaultOptions)).toBe(
`name,age,city\r\nAlice,30,\r\nBob,,Paris`
);
});
it('filters out empty objects', () => {
const input = JSON.stringify([{}, { name: 'Alice' }]);
expect(convertJsonToCsv(input, defaultOptions)).toBe(`name\r\nAlice`);
});
});
describe('quoting', () => {
it('quotes cells containing the delimiter', () => {
const input = JSON.stringify([{ name: 'Smith, John' }]);
expect(convertJsonToCsv(input, defaultOptions)).toBe(
`name\r\n"Smith, John"`
);
});
it('escapes double-quotes by doubling them', () => {
const input = JSON.stringify([{ name: 'He said "hello"' }]);
expect(convertJsonToCsv(input, defaultOptions)).toBe(
`name\r\n"He said ""hello"""`
);
});
it('quotes all cells when quoteStrings is always', () => {
const input = JSON.stringify([{ name: 'Alice', age: 30 }]);
expect(
convertJsonToCsv(input, { ...defaultOptions, quoteStrings: 'always' })
).toBe(`"name","age"\r\n"Alice","30"`);
});
it('quotes cells containing newlines', () => {
const input = JSON.stringify([{ notes: 'line1\nline2' }]);
expect(convertJsonToCsv(input, defaultOptions)).toBe(
`notes\r\n"line1\nline2"`
);
});
});
describe('delimiters', () => {
it('uses semicolon as delimiter', () => {
const input = JSON.stringify([{ name: 'Alice', age: 30 }]);
expect(
convertJsonToCsv(input, { ...defaultOptions, delimiter: ';' })
).toBe(`name;age\r\nAlice;30`);
});
it('uses tab as delimiter', () => {
const input = JSON.stringify([{ name: 'Alice', age: 30 }]);
expect(
convertJsonToCsv(input, { ...defaultOptions, delimiter: '\t' })
).toBe(`name\tage\r\nAlice\t30`);
});
});
describe('null and undefined values', () => {
it('converts null values to empty strings', () => {
const input = JSON.stringify([{ name: 'Alice', age: null }]);
expect(convertJsonToCsv(input, defaultOptions)).toBe(
`name,age\r\nAlice,`
);
});
});
describe('errors', () => {
it('throws on invalid JSON', () => {
expect(() => convertJsonToCsv('invalid json', defaultOptions)).toThrow(
'Invalid JSON input.'
);
});
it('throws on bare primitive', () => {
expect(() => convertJsonToCsv('42', defaultOptions)).toThrow(
'JSON input must be an object or array of objects, not a bare primitive.'
);
});
it('throws when no data rows are found', () => {
expect(() =>
convertJsonToCsv(JSON.stringify([{}, {}]), defaultOptions)
).toThrow('No data found in the provided JSON.');
});
});
});
+120
View File
@@ -0,0 +1,120 @@
import { InitialValuesType } from './types';
import { getJsonHeaders } from 'utils/json';
/**
* Recursively flattens any JSON value into a flat object.
* Objects → dot notation
* Arrays → index notation
*/
function flattenRecursive(
value: unknown,
prefix = '',
result: Record<string, string> = {}
): Record<string, string> {
if (value === null || value === undefined) {
if (prefix) result[prefix] = '';
return result;
}
if (typeof value !== 'object') {
if (prefix) result[prefix] = String(value);
return result;
}
if (Array.isArray(value)) {
value.forEach((item, index) => {
const newKey = prefix ? `${prefix}[${index}]` : `[${index}]`;
flattenRecursive(item, newKey, result);
});
return result;
}
for (const [key, val] of Object.entries(value)) {
const newKey = prefix ? `${prefix}.${key}` : key;
flattenRecursive(val, newKey, result);
}
return result;
}
/**
* Converts any JSON structure into row objects.
*/
function flattenToRows(json: unknown): Record<string, string>[] {
if (Array.isArray(json)) {
return json.map((item) => flattenRecursive(item));
}
if (typeof json === 'object' && json !== null) {
return [flattenRecursive(json)];
}
throw new Error(
'JSON input must be an object or array of objects, not a bare primitive.'
);
}
/**
* Escapes and quotes CSV cells according to options
*/
function quoteCell(value: string, options: InitialValuesType): string {
const { delimiter, quoteStrings } = options;
const escaped = value.replace(/"/g, '""');
const needsQuoting =
value.includes(delimiter) ||
value.includes('"') ||
value.includes('\n') ||
value.includes('\r');
if (quoteStrings === 'always') return `"${escaped}"`;
return needsQuoting ? `"${escaped}"` : value;
}
/**
* Converts JSON string to CSV
*/
export function convertJsonToCsv(
input: string,
options: InitialValuesType
): string {
const { delimiter, includeHeaders } = options;
if (!delimiter) throw new Error('No CSV delimiter.');
let parsed: unknown;
try {
parsed = JSON.parse(input);
} catch {
throw new Error('Invalid JSON input.');
}
const rows = flattenToRows(parsed).filter(
(row) => Object.keys(row).length > 0
);
if (rows.length === 0) {
throw new Error('No data found in the provided JSON.');
}
const headers = getJsonHeaders(rows);
const lines: string[] = [];
if (includeHeaders) {
lines.push(headers.map((h) => quoteCell(h, options)).join(delimiter));
}
for (const row of rows) {
const line = headers
.map((header) => quoteCell(row[header] ?? '', options))
.join(delimiter);
lines.push(line);
}
return lines.join('\r\n');
}
@@ -0,0 +1,5 @@
export type InitialValuesType = {
delimiter: string;
includeHeaders: boolean;
quoteStrings: 'always' | 'auto';
};
@@ -2,17 +2,7 @@ import type { GenericCalcType } from './types';
const areaSphere: GenericCalcType = {
icon: 'ph:sphere-duotone',
keywords: [
'sphere',
'area',
'surface area',
'geometry',
'mathematics',
'radius',
'calculation',
'3D',
'shape'
],
keywords: ['geometry', 'radius', '3d'],
path: 'area-sphere',
formula: 'A = 4 * pi * r**2',
presets: [],
@@ -2,18 +2,7 @@ import type { GenericCalcType } from './types';
const volumeSphere: GenericCalcType = {
icon: 'gravity-ui:sphere',
keywords: [
'sphere',
'volume',
'geometry',
'mathematics',
'radius',
'diameter',
'calculation',
'3D',
'shape',
'capacity'
],
keywords: ['geometry', 'radius', 'diameter', 'capacity', '3d'],
i18n: {
name: 'number:sphereVolume.title',
description: 'number:sphereVolume.description',
@@ -4,18 +4,7 @@ import wire_gauge from '../../../../../datatables/data/wire_gauge';
const voltageDropInWire: GenericCalcType = {
icon: 'simple-icons:wire',
keywords: [
'voltage drop',
'cable',
'wire',
'electrical',
'resistance',
'power loss',
'conductor',
'resistivity',
'AWG',
'gauge'
],
keywords: ['awg', 'gauge', 'resistivity', 'conductor'],
path: 'cable-voltage-drop',
formula: 'x = (((p * L) / (A/10**6) ) *2) * I',
i18n: {
+1 -1
View File
@@ -19,7 +19,7 @@ allGenericCalcs.forEach((x) => {
defineTool('number', {
...x,
path: 'generic-calc/' + x.path,
keywords: ['calculator', 'math', ...x.keywords],
keywords: [...x.keywords],
component: lazy(importComponent2)
})
);
+1 -3
View File
@@ -6,13 +6,11 @@ export const tool = defineTool('pdf', {
icon: 'material-symbols:compress',
keywords: [
'pdf',
'compress',
'reduce',
'size',
'optimize',
'shrink',
'file size',
'file',
'ghostscript',
'secure',
'private',
@@ -1,38 +0,0 @@
import { render, screen, fireEvent } from '@testing-library/react';
import ConvertToPdf from './index';
import { vi } from 'vitest';
import '@testing-library/jest-dom';
describe('ConvertToPdf', () => {
it('renders with default state values (full, portrait hidden, no scale shown)', () => {
render(<ConvertToPdf title="Test PDF" />);
expect(screen.getByLabelText(/Full Size \(Same as Image\)/i)).toBeChecked();
expect(screen.queryByLabelText(/A4 Page/i)).toBeInTheDocument();
expect(screen.queryByLabelText(/Portrait/i)).not.toBeInTheDocument();
expect(screen.queryByText(/Scale image:/i)).not.toBeInTheDocument();
});
it('switches to A4 page type and shows orientation and scale', () => {
render(<ConvertToPdf title="Test PDF" />);
const a4Option = screen.getByLabelText(/A4 Page/i);
fireEvent.click(a4Option);
expect(a4Option).toBeChecked();
expect(screen.getByLabelText(/Portrait/i)).toBeChecked();
expect(screen.getByText(/Scale image:\s*100%/i)).toBeInTheDocument();
});
it('updates scale when slider moves (after switching to A4)', () => {
render(<ConvertToPdf title="Test PDF" />);
fireEvent.click(screen.getByLabelText(/A4 Page/i));
const slider = screen.getByRole('slider');
fireEvent.change(slider, { target: { value: 80 } });
expect(screen.getByText(/Scale image:\s*80%/i)).toBeInTheDocument();
});
});
+73 -46
View File
@@ -9,63 +9,71 @@ import {
} from '@mui/material';
import React, { useState } from 'react';
import ToolContent from '@components/ToolContent';
import ToolImageInput from 'components/input/ToolImageInput';
import ToolMultipleImageInput, {
MultiImageInput
} from '@components/input/ToolMultipleImageInput';
import ToolFileResult from 'components/result/ToolFileResult';
import { ToolComponentProps } from '@tools/defineTool';
import { FormValues, Orientation, PageType, initialValues } from './types';
import { Orientation, PageType, InitialValuesType, ImageSize } from './types';
import { buildPdf } from './service';
import { useTranslation } from 'react-i18next';
const initialFormValues: FormValues = initialValues;
const initialValues: InitialValuesType = {
pageType: 'full',
orientation: 'portrait',
scale: 100
};
export default function ConvertToPdf({ title }: ToolComponentProps) {
const [input, setInput] = useState<File | null>(null);
const { t } = useTranslation('pdf');
const [input, setInput] = useState<MultiImageInput[]>([]);
const [result, setResult] = useState<File | null>(null);
const [imageSize, setImageSize] = useState<{
widthMm: number;
heightMm: number;
widthPx: number;
heightPx: number;
} | null>(null);
const [imageSizes, setImageSizes] = useState<ImageSize[] | null>(null);
const [isProcessing, setIsProcessing] = useState<boolean>(false);
const compute = async (values: FormValues) => {
if (!input) return;
const { pdfFile, imageSize } = await buildPdf({
file: input,
pageType: values.pageType,
orientation: values.orientation,
scale: values.scale
});
setResult(pdfFile);
setImageSize(imageSize);
const compute = async (
optionsValues: InitialValuesType,
input: MultiImageInput[]
) => {
if (!input.length) return;
setIsProcessing(true);
try {
const files = input.sort((a, b) => a.order - b.order).map((i) => i.file);
const { pdfFile, imageSizes } = await buildPdf(files, optionsValues);
setResult(pdfFile);
setImageSizes(imageSizes);
} catch (error) {
throw new Error('Error converting image(s) to PDF:' + error);
} finally {
setIsProcessing(false);
}
};
return (
<ToolContent<FormValues, File | null>
<ToolContent
title={title}
input={input}
setInput={setInput}
initialValues={initialFormValues}
initialValues={initialValues}
compute={compute}
inputComponent={
<Box>
<ToolImageInput
<ToolMultipleImageInput
type="image"
value={input}
onChange={setInput}
accept={[
'image/png',
'image/jpeg',
'image/webp',
'image/tiff',
'image/gif',
'image/heic',
'image/heif',
'image/x-adobe-dng',
'image/x-canon-cr2',
'image/x-nikon-nef',
'image/x-sony-arw',
'image/vnd.adobe.photoshop'
'image/heif'
]}
title="Input Image"
title={t('convertToPdf.inputTitle')}
/>
</Box>
}
@@ -76,7 +84,9 @@ export default function ConvertToPdf({ title }: ToolComponentProps) {
component: (
<Stack spacing={4}>
<Box>
<Typography variant="h6">PDF Type</Typography>
<Typography variant="h6">
{t('convertToPdf.options.type')}
</Typography>
<RadioGroup
row
value={values.pageType}
@@ -87,27 +97,37 @@ export default function ConvertToPdf({ title }: ToolComponentProps) {
<FormControlLabel
value="full"
control={<Radio />}
label="Full Size (Same as Image)"
label={t('convertToPdf.options.fullsize')}
/>
<FormControlLabel
value="a4"
control={<Radio />}
label="A4 Page"
label={t('convertToPdf.options.a4')}
/>
</RadioGroup>
{values.pageType === 'full' && imageSize && (
<Typography variant="body2" color="text.secondary">
Image size: {imageSize.widthMm.toFixed(1)} ×{' '}
{imageSize.heightMm.toFixed(1)} mm ({imageSize.widthPx} ×{' '}
{imageSize.heightPx} px)
</Typography>
{values.pageType === 'full' && imageSizes && (
<Stack spacing={1} mt={1}>
{imageSizes.map((size, index) => (
<Typography
key={index}
variant="body2"
color="text.secondary"
>
{t('convertToPdf.options.image')} {index + 1}:{' '}
{size.widthMm.toFixed(1)} × {size.heightMm.toFixed(1)}{' '}
mm ({size.widthPx} × {size.heightPx} px)
</Typography>
))}
</Stack>
)}
</Box>
{values.pageType === 'a4' && (
<Box>
<Typography variant="h6">Orientation</Typography>
<Typography variant="h6">
{t('convertToPdf.options.orientation')}
</Typography>
<RadioGroup
row
value={values.orientation}
@@ -121,12 +141,12 @@ export default function ConvertToPdf({ title }: ToolComponentProps) {
<FormControlLabel
value="portrait"
control={<Radio />}
label="Portrait (Vertical)"
label={t('convertToPdf.options.portrait')}
/>
<FormControlLabel
value="landscape"
control={<Radio />}
label="Landscape (Horizontal)"
label={t('convertToPdf.options.landscape')}
/>
</RadioGroup>
</Box>
@@ -134,8 +154,10 @@ export default function ConvertToPdf({ title }: ToolComponentProps) {
{values.pageType === 'a4' && (
<Box>
<Typography variant="h6">Scale</Typography>
<Typography>Scale image: {values.scale}%</Typography>
<Typography variant="h6">
{t('convertToPdf.options.scale')}
</Typography>
<Typography>{values.scale}%</Typography>
<Slider
value={values.scale}
onChange={(_, v) => updateField('scale', v as number)}
@@ -152,7 +174,12 @@ export default function ConvertToPdf({ title }: ToolComponentProps) {
] as const;
}}
resultComponent={
<ToolFileResult title="Output PDF" value={result} extension="pdf" />
<ToolFileResult
title={t('convertToPdf.resultTitle')}
value={result}
extension="pdf"
loading={isProcessing}
/>
}
/>
);
+2 -6
View File
@@ -19,14 +19,10 @@ export const tool = defineTool('pdf', {
'jpeg',
'png',
'gif',
'tiff',
'webp',
'heic',
'raw',
'psd',
'svg',
'quality',
'compression'
'heif',
'quality'
],
component: lazy(() => import('./index'))
});
+127 -45
View File
@@ -1,40 +1,98 @@
import jsPDF from 'jspdf';
import { Orientation, PageType, ImageSize } from './types';
import { ImageSize, InitialValuesType, LoadedImage } from './types';
import { heicTo, isHeic } from 'heic-to';
export interface ComputeOptions {
file: File;
pageType: PageType;
orientation: Orientation;
scale: number; // 10..100 (only applied for A4)
const pxToMm = (px: number) => px * 0.264583;
function returnImageFormat(file: File): 'PNG' | 'JPEG' {
const type = file.type.toLowerCase();
const name = file.name.toLowerCase();
if (type.includes('png') || name.endsWith('.png')) return 'PNG';
if (
type.includes('jpeg') ||
type.includes('jpg') ||
name.endsWith('.jpg') ||
name.endsWith('.jpeg')
) {
return 'JPEG';
}
if (type.includes('webp') || name.endsWith('.webp')) return 'JPEG';
if (type.includes('gif') || name.endsWith('.gif')) return 'JPEG';
console.warn(`Unsupported format: ${file.type}, defaulting to JPEG`);
return 'JPEG';
}
export interface ComputeResult {
pdfFile: File;
imageSize: ImageSize;
async function loadImage(file: File): Promise<LoadedImage> {
let processedFile = file;
if (await isHeic(file)) {
const convertedBlob = await heicTo({
blob: file,
type: 'image/jpeg'
});
processedFile = new File([convertedBlob], file.name + '.jpg', {
type: 'image/jpeg'
});
}
const image = new Image();
const objectUrl = URL.createObjectURL(processedFile);
image.src = objectUrl;
await image.decode();
return {
image,
objectUrl,
format: returnImageFormat(file),
filename: processedFile.name
};
}
export async function buildPdf({
file,
pageType,
orientation,
scale
}: ComputeOptions): Promise<ComputeResult> {
const img = new Image();
img.src = URL.createObjectURL(file);
export async function buildPdf(
files: File[],
options: InitialValuesType
): Promise<{ pdfFile: File; imageSizes: ImageSize[] }> {
if (!files.length) {
throw new Error('No files selected');
}
const { pageType, orientation, scale } = options;
let loadedImages: LoadedImage[] = [];
const imageSizes: ImageSize[] = [];
const results = await Promise.all(
files.map((file) =>
loadImage(file).catch((err) => {
console.warn(`Skipping ${file.name}:`, err);
return null;
})
)
);
loadedImages = results.filter((img): img is LoadedImage => img !== null);
if (!loadedImages.length) {
throw new Error('No images could be loaded');
}
const { image: firstImage, filename: firstImageFileName } = loadedImages[0];
const firstImageWidthMm = pxToMm(firstImage.width);
const firstImageHeightMm = pxToMm(firstImage.height);
try {
await img.decode();
const pxToMm = (px: number) => px * 0.264583;
const imgWidthMm = pxToMm(img.width);
const imgHeightMm = pxToMm(img.height);
const pdf =
pageType === 'full'
? new jsPDF({
orientation: imgWidthMm > imgHeightMm ? 'landscape' : 'portrait',
orientation:
firstImageWidthMm > firstImageHeightMm ? 'landscape' : 'portrait',
unit: 'mm',
format: [imgWidthMm, imgHeightMm]
format: [firstImageWidthMm, firstImageHeightMm]
})
: new jsPDF({
orientation,
@@ -44,37 +102,61 @@ export async function buildPdf({
pdf.setDisplayMode('fullwidth');
const pageWidth = pdf.internal.pageSize.getWidth();
const pageHeight = pdf.internal.pageSize.getHeight();
for (let index = 0; index < loadedImages.length; index += 1) {
const { image, format } = loadedImages[index];
const imageWidthMm = pxToMm(image.width);
const imageHeightMm = pxToMm(image.height);
const widthRatio = pageWidth / img.width;
const heightRatio = pageHeight / img.height;
const fitScale = Math.min(widthRatio, heightRatio);
if (index > 0) {
if (pageType === 'full') {
pdf.addPage(
[imageWidthMm, imageHeightMm],
imageWidthMm > imageHeightMm ? 'landscape' : 'portrait'
);
} else {
pdf.addPage('a4', orientation);
}
}
const finalWidth =
pageType === 'full' ? pageWidth : img.width * fitScale * (scale / 100);
const pageWidth = pdf.internal.pageSize.getWidth();
const pageHeight = pdf.internal.pageSize.getHeight();
const finalHeight =
pageType === 'full' ? pageHeight : img.height * fitScale * (scale / 100);
const widthRatio = pageWidth / image.width;
const heightRatio = pageHeight / image.height;
const fitScale = Math.min(widthRatio, heightRatio);
const x = pageType === 'full' ? 0 : (pageWidth - finalWidth) / 2;
const y = pageType === 'full' ? 0 : (pageHeight - finalHeight) / 2;
const finalWidth =
pageType === 'full'
? pageWidth
: image.width * fitScale * (scale / 100);
pdf.addImage(img, 'JPEG', x, y, finalWidth, finalHeight);
const finalHeight =
pageType === 'full'
? pageHeight
: image.height * fitScale * (scale / 100);
const x = pageType === 'full' ? 0 : (pageWidth - finalWidth) / 2;
const y = pageType === 'full' ? 0 : (pageHeight - finalHeight) / 2;
pdf.addImage(image, format, x, y, finalWidth, finalHeight);
imageSizes.push({
widthMm: imageWidthMm,
heightMm: imageHeightMm,
widthPx: image.width,
heightPx: image.height
});
}
const blob = pdf.output('blob');
const fileName = file.name.replace(/\.[^/.]+$/, '') + '.pdf';
const firstFileName = firstImageFileName.replace(/\.[^/.]+$/, '');
const fileName =
files.length === 1 ? `${firstFileName}.pdf` : 'merged-images.pdf';
return {
pdfFile: new File([blob], fileName, { type: 'application/pdf' }),
imageSize: {
widthMm: imgWidthMm,
heightMm: imgHeightMm,
widthPx: img.width,
heightPx: img.height
}
imageSizes: imageSizes
};
} finally {
URL.revokeObjectURL(img.src);
loadedImages.forEach(({ objectUrl }) => URL.revokeObjectURL(objectUrl));
}
}
+7 -6
View File
@@ -8,14 +8,15 @@ export interface ImageSize {
heightPx: number;
}
export interface FormValues {
export interface InitialValuesType {
pageType: PageType;
orientation: Orientation;
scale: number;
}
export const initialValues: FormValues = {
pageType: 'full',
orientation: 'portrait',
scale: 100
};
export interface LoadedImage {
image: HTMLImageElement;
objectUrl: string;
format: 'PNG' | 'JPEG';
filename: string;
}

Some files were not shown because too many files have changed in this diff Show More