Merge branch 'V2' into nginx-mjs-fix

This commit is contained in:
Reece Browne 2025-09-25 16:49:35 +01:00 committed by GitHub
commit ab099eb926
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 584 additions and 77 deletions

View File

@ -357,222 +357,277 @@
"globalPopularity": "Global Popularity", "globalPopularity": "Global Popularity",
"sortBy": "Sort by:", "sortBy": "Sort by:",
"multiTool": { "multiTool": {
"tags": "multiple,tools",
"title": "PDF Multi Tool", "title": "PDF Multi Tool",
"desc": "Merge, Rotate, Rearrange, Split, and Remove pages" "desc": "Merge, Rotate, Rearrange, Split, and Remove pages"
}, },
"merge": { "merge": {
"tags": "combine,join,unite",
"title": "Merge", "title": "Merge",
"desc": "Easily merge multiple PDFs into one." "desc": "Easily merge multiple PDFs into one."
}, },
"split": { "split": {
"tags": "divide,separate,break",
"title": "Split", "title": "Split",
"desc": "Split PDFs into multiple documents" "desc": "Split PDFs into multiple documents"
}, },
"rotate": { "rotate": {
"tags": "turn,flip,orient",
"title": "Rotate", "title": "Rotate",
"desc": "Easily rotate your PDFs." "desc": "Easily rotate your PDFs."
}, },
"convert": { "convert": {
"tags": "transform,change",
"title": "Convert", "title": "Convert",
"desc": "Convert files between different formats" "desc": "Convert files between different formats"
}, },
"pdfOrganiser": { "pdfOrganiser": {
"tags": "organize,rearrange,reorder",
"title": "Organise", "title": "Organise",
"desc": "Remove/Rearrange pages in any order" "desc": "Remove/Rearrange pages in any order"
}, },
"addImage": { "addImage": {
"tags": "insert,embed,place",
"title": "Add image", "title": "Add image",
"desc": "Adds a image onto a set location on the PDF" "desc": "Adds a image onto a set location on the PDF"
}, },
"addAttachments": { "addAttachments": {
"tags": "embed,attach,include",
"title": "Add Attachments", "title": "Add Attachments",
"desc": "Add or remove embedded files (attachments) to/from a PDF" "desc": "Add or remove embedded files (attachments) to/from a PDF"
}, },
"watermark": { "watermark": {
"tags": "stamp,mark,overlay",
"title": "Add Watermark", "title": "Add Watermark",
"desc": "Add a custom watermark to your PDF document." "desc": "Add a custom watermark to your PDF document."
}, },
"removePassword": { "removePassword": {
"tags": "unlock",
"title": "Remove Password", "title": "Remove Password",
"desc": "Remove password protection from your PDF document." "desc": "Remove password protection from your PDF document."
}, },
"compress": { "compress": {
"tags": "shrink,reduce,optimize",
"title": "Compress", "title": "Compress",
"desc": "Compress PDFs to reduce their file size." "desc": "Compress PDFs to reduce their file size."
}, },
"unlockPDFForms": { "unlockPDFForms": {
"tags": "unlock,enable,edit",
"title": "Unlock PDF Forms", "title": "Unlock PDF Forms",
"desc": "Remove read-only property of form fields in a PDF document." "desc": "Remove read-only property of form fields in a PDF document."
}, },
"changeMetadata": { "changeMetadata": {
"tags": "edit,modify,update",
"title": "Change Metadata", "title": "Change Metadata",
"desc": "Change/Remove/Add metadata from a PDF document" "desc": "Change/Remove/Add metadata from a PDF document"
}, },
"ocr": { "ocr": {
"tags": "extract,scan",
"title": "OCR / Cleanup scans", "title": "OCR / Cleanup scans",
"desc": "Cleanup scans and detects text from images within a PDF and re-adds it as text." "desc": "Cleanup scans and detects text from images within a PDF and re-adds it as text."
}, },
"extractImages": { "extractImages": {
"tags": "pull,save,export",
"title": "Extract Images", "title": "Extract Images",
"desc": "Extracts all images from a PDF and saves them to zip" "desc": "Extracts all images from a PDF and saves them to zip"
}, },
"scannerImageSplit": { "scannerImageSplit": {
"tags": "detect,split,photos",
"title": "Detect/Split Scanned photos", "title": "Detect/Split Scanned photos",
"desc": "Splits multiple photos from within a photo/PDF" "desc": "Splits multiple photos from within a photo/PDF"
}, },
"sign": { "sign": {
"tags": "signature,autograph",
"title": "Sign", "title": "Sign",
"desc": "Adds signature to PDF by drawing, text or image" "desc": "Adds signature to PDF by drawing, text or image"
}, },
"flatten": { "flatten": {
"tags": "simplify,remove,interactive",
"title": "Flatten", "title": "Flatten",
"desc": "Remove all interactive elements and forms from a PDF" "desc": "Remove all interactive elements and forms from a PDF"
}, },
"certSign": { "certSign": {
"tags": "authenticate,PEM,P12,official,encrypt,sign,certificate,PKCS12,JKS,server,manual,auto",
"title": "Sign with Certificate", "title": "Sign with Certificate",
"desc": "Signs a PDF with a Certificate/Key (PEM/P12)" "desc": "Signs a PDF with a Certificate/Key (PEM/P12)"
}, },
"repair": { "repair": {
"tags": "fix,restore",
"title": "Repair", "title": "Repair",
"desc": "Tries to repair a corrupt/broken PDF" "desc": "Tries to repair a corrupt/broken PDF"
}, },
"removeBlanks": { "removeBlanks": {
"tags": "delete,clean,empty",
"title": "Remove Blank pages", "title": "Remove Blank pages",
"desc": "Detects and removes blank pages from a document" "desc": "Detects and removes blank pages from a document"
}, },
"removeAnnotations": { "removeAnnotations": {
"tags": "delete,clean,strip",
"title": "Remove Annotations", "title": "Remove Annotations",
"desc": "Removes all comments/annotations from a PDF" "desc": "Removes all comments/annotations from a PDF"
}, },
"compare": { "compare": {
"tags": "difference",
"title": "Compare", "title": "Compare",
"desc": "Compares and shows the differences between 2 PDF Documents" "desc": "Compares and shows the differences between 2 PDF Documents"
}, },
"removeCertSign": { "removeCertSign": {
"tags": "remove,delete,unlock",
"title": "Remove Certificate Sign", "title": "Remove Certificate Sign",
"desc": "Remove certificate signature from PDF" "desc": "Remove certificate signature from PDF"
}, },
"pageLayout": { "pageLayout": {
"tags": "layout,arrange,combine",
"title": "Multi-Page Layout", "title": "Multi-Page Layout",
"desc": "Merge multiple pages of a PDF document into a single page" "desc": "Merge multiple pages of a PDF document into a single page"
}, },
"bookletImposition": { "bookletImposition": {
"tags": "booklet,print,binding",
"title": "Booklet Imposition", "title": "Booklet Imposition",
"desc": "Create booklets with proper page ordering and multi-page layout for printing and binding" "desc": "Create booklets with proper page ordering and multi-page layout for printing and binding"
}, },
"scalePages": { "scalePages": {
"tags": "resize,adjust,scale",
"title": "Adjust page size/scale", "title": "Adjust page size/scale",
"desc": "Change the size/scale of a page and/or its contents." "desc": "Change the size/scale of a page and/or its contents."
}, },
"addPageNumbers": { "addPageNumbers": {
"tags": "number,pagination,count",
"title": "Add Page Numbers", "title": "Add Page Numbers",
"desc": "Add Page numbers throughout a document in a set location" "desc": "Add Page numbers throughout a document in a set location"
}, },
"autoRename": { "autoRename": {
"tags": "auto-detect,header-based,organize,relabel",
"title": "Auto Rename PDF File", "title": "Auto Rename PDF File",
"desc": "Auto renames a PDF file based on its detected header" "desc": "Auto renames a PDF file based on its detected header"
}, },
"adjustContrast": { "adjustContrast": {
"tags": "contrast,brightness,saturation",
"title": "Adjust Colours/Contrast", "title": "Adjust Colours/Contrast",
"desc": "Adjust Contrast, Saturation and Brightness of a PDF" "desc": "Adjust Contrast, Saturation and Brightness of a PDF"
}, },
"crop": { "crop": {
"tags": "trim,cut,resize",
"title": "Crop PDF", "title": "Crop PDF",
"desc": "Crop a PDF to reduce its size (maintains text!)" "desc": "Crop a PDF to reduce its size (maintains text!)"
}, },
"autoSplitPDF": { "autoSplitPDF": {
"tags": "auto,split,QR",
"title": "Auto Split Pages", "title": "Auto Split Pages",
"desc": "Auto Split Scanned PDF with physical scanned page splitter QR Code" "desc": "Auto Split Scanned PDF with physical scanned page splitter QR Code"
}, },
"sanitize": { "sanitize": {
"tags": "clean,purge,remove",
"title": "Sanitise", "title": "Sanitise",
"desc": "Remove potentially harmful elements from PDF files" "desc": "Remove potentially harmful elements from PDF files"
}, },
"getPdfInfo": { "getPdfInfo": {
"tags": "info,metadata,details",
"title": "Get ALL Info on PDF", "title": "Get ALL Info on PDF",
"desc": "Grabs any and all information possible on PDFs" "desc": "Grabs any and all information possible on PDFs"
}, },
"pdfToSinglePage": { "pdfToSinglePage": {
"tags": "combine,merge,single",
"title": "PDF to Single Large Page", "title": "PDF to Single Large Page",
"desc": "Merges all PDF pages into one large single page" "desc": "Merges all PDF pages into one large single page"
}, },
"showJS": { "showJS": {
"tags": "javascript,code,script",
"title": "Show Javascript", "title": "Show Javascript",
"desc": "Searches and displays any JS injected into a PDF" "desc": "Searches and displays any JS injected into a PDF"
}, },
"redact": { "redact": {
"tags": "censor,blackout,hide",
"title": "Redact", "title": "Redact",
"desc": "Redacts (blacks out) a PDF based on selected text, drawn shapes and/or selected page(s)" "desc": "Redacts (blacks out) a PDF based on selected text, drawn shapes and/or selected page(s)"
}, },
"overlayPdfs": { "overlayPdfs": {
"tags": "overlay,combine,stack",
"title": "Overlay PDFs", "title": "Overlay PDFs",
"desc": "Overlays PDFs on-top of another PDF" "desc": "Overlays PDFs on-top of another PDF"
}, },
"splitBySections": { "splitBySections": {
"tags": "split,sections,divide",
"title": "Split PDF by Sections", "title": "Split PDF by Sections",
"desc": "Divide each page of a PDF into smaller horizontal and vertical sections" "desc": "Divide each page of a PDF into smaller horizontal and vertical sections"
}, },
"addStamp": { "addStamp": {
"tags": "stamp,mark,seal",
"title": "Add Stamp to PDF", "title": "Add Stamp to PDF",
"desc": "Add text or add image stamps at set locations" "desc": "Add text or add image stamps at set locations"
}, },
"removeImage": { "removeImage": {
"tags": "remove,delete,clean",
"title": "Remove image", "title": "Remove image",
"desc": "Remove image from PDF to reduce file size" "desc": "Remove image from PDF to reduce file size"
}, },
"splitByChapters": { "splitByChapters": {
"tags": "split,chapters,structure",
"title": "Split PDF by Chapters", "title": "Split PDF by Chapters",
"desc": "Split a PDF into multiple files based on its chapter structure." "desc": "Split a PDF into multiple files based on its chapter structure."
}, },
"validateSignature": { "validateSignature": {
"tags": "validate,verify,certificate",
"title": "Validate PDF Signature", "title": "Validate PDF Signature",
"desc": "Verify digital signatures and certificates in PDF documents" "desc": "Verify digital signatures and certificates in PDF documents"
}, },
"swagger": { "swagger": {
"tags": "API,documentation,test",
"title": "API Documentation", "title": "API Documentation",
"desc": "View API documentation and test endpoints" "desc": "View API documentation and test endpoints"
}, },
"fakeScan": { "fakeScan": {
"tags": "scan,simulate,create",
"title": "Fake Scan", "title": "Fake Scan",
"desc": "Create a PDF that looks like it was scanned" "desc": "Create a PDF that looks like it was scanned"
}, },
"editTableOfContents": { "editTableOfContents": {
"tags": "bookmarks,contents,edit",
"title": "Edit Table of Contents", "title": "Edit Table of Contents",
"desc": "Add or edit bookmarks and table of contents in PDF documents" "desc": "Add or edit bookmarks and table of contents in PDF documents"
}, },
"manageCertificates": { "manageCertificates": {
"tags": "certificates,import,export",
"title": "Manage Certificates", "title": "Manage Certificates",
"desc": "Import, export, or delete digital certificate files used for signing PDFs." "desc": "Import, export, or delete digital certificate files used for signing PDFs."
}, },
"read": { "read": {
"title": "Read", "tags": "view,open,display",
"title": "Read",
"desc": "View and annotate PDFs. Highlight text, draw, or insert comments for review and collaboration." "desc": "View and annotate PDFs. Highlight text, draw, or insert comments for review and collaboration."
}, },
"reorganizePages": { "reorganizePages": {
"tags": "rearrange,reorder,organize",
"title": "Reorganize Pages", "title": "Reorganize Pages",
"desc": "Rearrange, duplicate, or delete PDF pages with visual drag-and-drop control." "desc": "Rearrange, duplicate, or delete PDF pages with visual drag-and-drop control."
}, },
"extractPages": { "extractPages": {
"tags": "pull,select,copy",
"title": "Extract Pages", "title": "Extract Pages",
"desc": "Extract specific pages from a PDF document" "desc": "Extract specific pages from a PDF document"
}, },
"removePages": { "removePages": {
"tags": "delete,extract,exclude",
"title": "Remove Pages", "title": "Remove Pages",
"desc": "Remove specific pages from a PDF document" "desc": "Remove specific pages from a PDF document"
}, },
"autoSizeSplitPDF": { "autoSizeSplitPDF": {
"tags": "auto,split,size",
"title": "Auto Split by Size/Count", "title": "Auto Split by Size/Count",
"desc": "Automatically split PDFs by file size or page count" "desc": "Automatically split PDFs by file size or page count"
}, },
"replaceColorPdf": { "replaceColorPdf": {
"tags": "color,replace,invert",
"title": "Replace & Invert Colour", "title": "Replace & Invert Colour",
"desc": "Replace or invert colours in PDF documents" "desc": "Replace or invert colours in PDF documents"
}, },
"devApi": { "devApi": {
"tags": "API,development,documentation",
"title": "API", "title": "API",
"desc": "Link to API documentation" "desc": "Link to API documentation"
}, },
"devFolderScanning": { "devFolderScanning": {
"tags": "automation,folder,scanning",
"title": "Automated Folder Scanning", "title": "Automated Folder Scanning",
"desc": "Link to automated folder scanning guide" "desc": "Link to automated folder scanning guide"
}, },
@ -593,6 +648,7 @@
"desc": "Change document restrictions and permissions" "desc": "Change document restrictions and permissions"
}, },
"automate": { "automate": {
"tags": "workflow,sequence,automation",
"title": "Automate", "title": "Automate",
"desc": "Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks." "desc": "Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks."
} }
@ -659,7 +715,6 @@
} }
}, },
"split": { "split": {
"tags": "Page operations,divide,Multi Page,cut,server side",
"title": "Split PDF", "title": "Split PDF",
"header": "Split PDF", "header": "Split PDF",
"desc": { "desc": {
@ -785,7 +840,6 @@
} }
}, },
"rotate": { "rotate": {
"tags": "server side",
"title": "Rotate PDF", "title": "Rotate PDF",
"submit": "Apply Rotation", "submit": "Apply Rotation",
"error": { "error": {
@ -1298,7 +1352,6 @@
} }
}, },
"changeMetadata": { "changeMetadata": {
"tags": "Title,author,date,creation,time,publisher,producer,stats",
"header": "Change Metadata", "header": "Change Metadata",
"submit": "Change", "submit": "Change",
"filenamePrefix": "metadata", "filenamePrefix": "metadata",
@ -1613,7 +1666,6 @@
"info": "Python is not installed. It is required to run." "info": "Python is not installed. It is required to run."
}, },
"sign": { "sign": {
"tags": "authorize,initials,drawn-signature,text-sign,image-signature",
"title": "Sign", "title": "Sign",
"header": "Sign PDFs", "header": "Sign PDFs",
"upload": "Upload Image", "upload": "Upload Image",
@ -1637,7 +1689,6 @@
"redo": "Redo" "redo": "Redo"
}, },
"flatten": { "flatten": {
"tags": "static,deactivate,non-interactive,streamline",
"title": "Flatten", "title": "Flatten",
"header": "Flatten PDF", "header": "Flatten PDF",
"flattenOnlyForms": "Flatten only forms", "flattenOnlyForms": "Flatten only forms",
@ -1702,7 +1753,6 @@
} }
}, },
"removeBlanks": { "removeBlanks": {
"tags": "cleanup,streamline,non-content,organize",
"title": "Remove Blanks", "title": "Remove Blanks",
"header": "Remove Blank Pages", "header": "Remove Blank Pages",
"settings": { "settings": {
@ -2099,7 +2149,6 @@
"tags": "color-correction,tune,modify,enhance,colour-correction" "tags": "color-correction,tune,modify,enhance,colour-correction"
}, },
"crop": { "crop": {
"tags": "trim,shrink,edit,shape",
"title": "Crop", "title": "Crop",
"header": "Crop PDF", "header": "Crop PDF",
"submit": "Apply Crop", "submit": "Apply Crop",

View File

@ -348,206 +348,257 @@
"globalPopularity": "Global Popularity", "globalPopularity": "Global Popularity",
"sortBy": "Sort by:", "sortBy": "Sort by:",
"multiTool": { "multiTool": {
"tags": "multiple,tools",
"title": "PDF Multi Tool", "title": "PDF Multi Tool",
"desc": "Merge, Rotate, Rearrange, Split, and Remove pages" "desc": "Merge, Rotate, Rearrange, Split, and Remove pages"
}, },
"merge": { "merge": {
"tags": "combine,join,unite",
"title": "Merge", "title": "Merge",
"desc": "Easily merge multiple PDFs into one." "desc": "Easily merge multiple PDFs into one."
}, },
"split": { "split": {
"tags": "divide,separate,break",
"title": "Split", "title": "Split",
"desc": "Split PDFs into multiple documents" "desc": "Split PDFs into multiple documents"
}, },
"rotate": { "rotate": {
"tags": "turn,flip,orient",
"title": "Rotate", "title": "Rotate",
"desc": "Easily rotate your PDFs." "desc": "Easily rotate your PDFs."
}, },
"imageToPDF": { "imageToPDF": {
"tags": "convert,image,transform",
"title": "Image to PDF", "title": "Image to PDF",
"desc": "Convert a image (PNG, JPEG, GIF) to PDF." "desc": "Convert a image (PNG, JPEG, GIF) to PDF."
}, },
"pdfToImage": { "pdfToImage": {
"tags": "convert,image,extract",
"title": "PDF to Image", "title": "PDF to Image",
"desc": "Convert a PDF to a image. (PNG, JPEG, GIF)" "desc": "Convert a PDF to a image. (PNG, JPEG, GIF)"
}, },
"pdfOrganiser": { "pdfOrganiser": {
"tags": "organize,rearrange,reorder",
"title": "Organize", "title": "Organize",
"desc": "Remove/Rearrange pages in any order" "desc": "Remove/Rearrange pages in any order"
}, },
"addImage": { "addImage": {
"tags": "insert,embed,place",
"title": "Add image", "title": "Add image",
"desc": "Adds a image onto a set location on the PDF" "desc": "Adds a image onto a set location on the PDF"
}, },
"watermark": { "watermark": {
"tags": "stamp,mark,overlay",
"title": "Add Watermark", "title": "Add Watermark",
"desc": "Add a custom watermark to your PDF document." "desc": "Add a custom watermark to your PDF document."
}, },
"permissions": { "permissions": {
"tags": "permissions,security,access",
"title": "Change Permissions", "title": "Change Permissions",
"desc": "Change the permissions of your PDF document" "desc": "Change the permissions of your PDF document"
}, },
"pageRemover": { "pageRemover": {
"tags": "remove,delete,pages",
"title": "Remove", "title": "Remove",
"desc": "Delete unwanted pages from your PDF document." "desc": "Delete unwanted pages from your PDF document."
}, },
"addPassword": { "addPassword": {
"tags": "password,encrypt,secure",
"title": "Add Password", "title": "Add Password",
"desc": "Encrypt your PDF document with a password." "desc": "Encrypt your PDF document with a password."
}, },
"changePermissions": { "changePermissions": {
"tags": "permissions,restrictions,security",
"title": "Change Permissions", "title": "Change Permissions",
"desc": "Change document restrictions and permissions." "desc": "Change document restrictions and permissions."
}, },
"removePassword": { "removePassword": {
"tags": "unlock,remove,password",
"title": "Remove Password", "title": "Remove Password",
"desc": "Remove password protection from your PDF document." "desc": "Remove password protection from your PDF document."
}, },
"compress": { "compress": {
"tags": "shrink,reduce,optimize",
"title": "Compress", "title": "Compress",
"desc": "Compress PDFs to reduce their file size." "desc": "Compress PDFs to reduce their file size."
}, },
"sanitize": { "sanitize": {
"tags": "clean,purge,remove",
"title": "Sanitize", "title": "Sanitize",
"desc": "Remove potentially harmful elements from PDF files." "desc": "Remove potentially harmful elements from PDF files."
}, },
"unlockPDFForms": { "unlockPDFForms": {
"tags": "unlock,enable,edit",
"title": "Unlock PDF Forms", "title": "Unlock PDF Forms",
"desc": "Remove read-only property of form fields in a PDF document." "desc": "Remove read-only property of form fields in a PDF document."
}, },
"changeMetadata": { "changeMetadata": {
"tags": "edit,modify,update",
"title": "Change Metadata", "title": "Change Metadata",
"desc": "Change/Remove/Add metadata from a PDF document" "desc": "Change/Remove/Add metadata from a PDF document"
}, },
"fileToPDF": { "fileToPDF": {
"tags": "convert,transform,change",
"title": "Convert file to PDF", "title": "Convert file to PDF",
"desc": "Convert nearly any file to PDF (DOCX, PNG, XLS, PPT, TXT and more)" "desc": "Convert nearly any file to PDF (DOCX, PNG, XLS, PPT, TXT and more)"
}, },
"ocr": { "ocr": {
"tags": "extract,scan",
"title": "OCR / Cleanup scans", "title": "OCR / Cleanup scans",
"desc": "Cleanup scans and detects text from images within a PDF and re-adds it as text." "desc": "Cleanup scans and detects text from images within a PDF and re-adds it as text."
}, },
"extractImages": { "extractImages": {
"tags": "pull,save,export",
"title": "Extract Images", "title": "Extract Images",
"desc": "Extracts all images from a PDF and saves them to zip" "desc": "Extracts all images from a PDF and saves them to zip"
}, },
"pdfToPDFA": { "pdfToPDFA": {
"tags": "convert,archive,long-term",
"title": "PDF to PDF/A", "title": "PDF to PDF/A",
"desc": "Convert PDF to PDF/A for long-term storage" "desc": "Convert PDF to PDF/A for long-term storage"
}, },
"PDFToWord": { "PDFToWord": {
"tags": "convert,word,doc",
"title": "PDF to Word", "title": "PDF to Word",
"desc": "Convert PDF to Word formats (DOC, DOCX and ODT)" "desc": "Convert PDF to Word formats (DOC, DOCX and ODT)"
}, },
"PDFToPresentation": { "PDFToPresentation": {
"tags": "convert,presentation,ppt",
"title": "PDF to Presentation", "title": "PDF to Presentation",
"desc": "Convert PDF to Presentation formats (PPT, PPTX and ODP)" "desc": "Convert PDF to Presentation formats (PPT, PPTX and ODP)"
}, },
"PDFToText": { "PDFToText": {
"tags": "convert,text,rtf",
"title": "PDF to RTF (Text)", "title": "PDF to RTF (Text)",
"desc": "Convert PDF to Text or RTF format" "desc": "Convert PDF to Text or RTF format"
}, },
"PDFToHTML": { "PDFToHTML": {
"tags": "convert,html,web",
"title": "PDF to HTML", "title": "PDF to HTML",
"desc": "Convert PDF to HTML format" "desc": "Convert PDF to HTML format"
}, },
"PDFToXML": { "PDFToXML": {
"tags": "convert,xml,data",
"title": "PDF to XML", "title": "PDF to XML",
"desc": "Convert PDF to XML format" "desc": "Convert PDF to XML format"
}, },
"ScannerImageSplit": { "ScannerImageSplit": {
"tags": "detect,split,photos",
"title": "Detect/Split Scanned photos", "title": "Detect/Split Scanned photos",
"desc": "Splits multiple photos from within a photo/PDF" "desc": "Splits multiple photos from within a photo/PDF"
}, },
"sign": { "sign": {
"tags": "signature,autograph",
"title": "Sign", "title": "Sign",
"desc": "Adds signature to PDF by drawing, text or image" "desc": "Adds signature to PDF by drawing, text or image"
}, },
"flatten": { "flatten": {
"tags": "simplify,remove,interactive",
"title": "Flatten", "title": "Flatten",
"desc": "Remove all interactive elements and forms from a PDF" "desc": "Remove all interactive elements and forms from a PDF"
}, },
"repair": { "repair": {
"tags": "fix,restore",
"title": "Repair", "title": "Repair",
"desc": "Tries to repair a corrupt/broken PDF" "desc": "Tries to repair a corrupt/broken PDF"
}, },
"removeBlanks": { "removeBlanks": {
"tags": "delete,clean,empty",
"title": "Remove Blank pages", "title": "Remove Blank pages",
"desc": "Detects and removes blank pages from a document" "desc": "Detects and removes blank pages from a document"
}, },
"removeAnnotations": { "removeAnnotations": {
"tags": "delete,clean,strip",
"title": "Remove Annotations", "title": "Remove Annotations",
"desc": "Removes all comments/annotations from a PDF" "desc": "Removes all comments/annotations from a PDF"
}, },
"compare": { "compare": {
"tags": "difference",
"title": "Compare", "title": "Compare",
"desc": "Compares and shows the differences between 2 PDF Documents" "desc": "Compares and shows the differences between 2 PDF Documents"
}, },
"certSign": { "certSign": {
"tags": "authenticate,PEM,P12,official,encrypt,sign,certificate,PKCS12,JKS,server,manual,auto",
"title": "Sign with Certificate", "title": "Sign with Certificate",
"desc": "Signs a PDF with a Certificate/Key (PEM/P12)" "desc": "Signs a PDF with a Certificate/Key (PEM/P12)"
}, },
"removeCertSign": { "removeCertSign": {
"tags": "remove,delete,unlock",
"title": "Remove Certificate Sign", "title": "Remove Certificate Sign",
"desc": "Remove certificate signature from PDF" "desc": "Remove certificate signature from PDF"
}, },
"pageLayout": { "pageLayout": {
"tags": "layout,arrange,combine",
"title": "Multi-Page Layout", "title": "Multi-Page Layout",
"desc": "Merge multiple pages of a PDF document into a single page" "desc": "Merge multiple pages of a PDF document into a single page"
}, },
"bookletImposition": { "bookletImposition": {
"tags": "booklet,print,binding",
"title": "Booklet Imposition", "title": "Booklet Imposition",
"desc": "Create booklets with proper page ordering and multi-page layout for printing and binding" "desc": "Create booklets with proper page ordering and multi-page layout for printing and binding"
}, },
"scalePages": { "scalePages": {
"tags": "resize,adjust,scale",
"title": "Adjust page size/scale", "title": "Adjust page size/scale",
"desc": "Change the size/scale of a page and/or its contents." "desc": "Change the size/scale of a page and/or its contents."
}, },
"pipeline": { "pipeline": {
"tags": "automation,script,workflow",
"title": "Pipeline", "title": "Pipeline",
"desc": "Run multiple actions on PDFs by defining pipeline scripts" "desc": "Run multiple actions on PDFs by defining pipeline scripts"
}, },
"addPageNumbers": { "addPageNumbers": {
"tags": "number,pagination,count",
"title": "Add Page Numbers", "title": "Add Page Numbers",
"desc": "Add Page numbers throughout a document in a set location" "desc": "Add Page numbers throughout a document in a set location"
}, },
"auto-rename": { "auto-rename": {
"tags": "auto-detect,header-based,organize,relabel",
"title": "Auto Rename PDF File", "title": "Auto Rename PDF File",
"desc": "Auto renames a PDF file based on its detected header" "desc": "Auto renames a PDF file based on its detected header"
}, },
"adjustContrast": { "adjustContrast": {
"tags": "contrast,brightness,saturation",
"title": "Adjust Colors/Contrast", "title": "Adjust Colors/Contrast",
"desc": "Adjust Contrast, Saturation and Brightness of a PDF" "desc": "Adjust Contrast, Saturation and Brightness of a PDF"
}, },
"crop": { "crop": {
"tags": "trim,cut,resize",
"title": "Crop PDF", "title": "Crop PDF",
"desc": "Crop a PDF to reduce its size (maintains text!)" "desc": "Crop a PDF to reduce its size (maintains text!)"
}, },
"autoSplitPDF": { "autoSplitPDF": {
"tags": "auto,split,QR",
"title": "Auto Split Pages", "title": "Auto Split Pages",
"desc": "Auto Split Scanned PDF with physical scanned page splitter QR Code" "desc": "Auto Split Scanned PDF with physical scanned page splitter QR Code"
}, },
"sanitizePDF": { "sanitizePDF": {
"tags": "clean,purge,remove",
"title": "Sanitize", "title": "Sanitize",
"desc": "Remove scripts and other elements from PDF files" "desc": "Remove scripts and other elements from PDF files"
}, },
"URLToPDF": { "URLToPDF": {
"tags": "convert,url,website",
"title": "URL/Website To PDF", "title": "URL/Website To PDF",
"desc": "Converts any http(s)URL to PDF" "desc": "Converts any http(s)URL to PDF"
}, },
"HTMLToPDF": { "HTMLToPDF": {
"tags": "convert,html,web",
"title": "HTML to PDF", "title": "HTML to PDF",
"desc": "Converts any HTML file or zip to PDF" "desc": "Converts any HTML file or zip to PDF"
}, },
"MarkdownToPDF": { "MarkdownToPDF": {
"tags": "convert,markdown,md",
"title": "Markdown to PDF", "title": "Markdown to PDF",
"desc": "Converts any Markdown file to PDF" "desc": "Converts any Markdown file to PDF"
}, },
"PDFToMarkdown": { "PDFToMarkdown": {
"tags": "convert,markdown,md",
"title": "PDF to Markdown", "title": "PDF to Markdown",
"desc": "Converts any PDF to Markdown" "desc": "Converts any PDF to Markdown"
}, },
"getPdfInfo": { "getPdfInfo": {
"tags": "info,metadata,details",
"title": "Get ALL Info on PDF", "title": "Get ALL Info on PDF",
"desc": "Grabs any and all information possible on PDFs" "desc": "Grabs any and all information possible on PDFs"
}, },
@ -564,50 +615,62 @@
"desc": "Searches and displays any JS injected into a PDF" "desc": "Searches and displays any JS injected into a PDF"
}, },
"autoRedact": { "autoRedact": {
"tags": "auto,redact,censor",
"title": "Auto Redact", "title": "Auto Redact",
"desc": "Auto Redacts(Blacks out) text in a PDF based on input text" "desc": "Auto Redacts(Blacks out) text in a PDF based on input text"
}, },
"redact": { "redact": {
"tags": "censor,blackout,hide",
"title": "Manual Redaction", "title": "Manual Redaction",
"desc": "Redacts a PDF based on selected text, drawn shapes and/or selected page(s)" "desc": "Redacts a PDF based on selected text, drawn shapes and/or selected page(s)"
}, },
"PDFToCSV": { "PDFToCSV": {
"tags": "convert,csv,table",
"title": "PDF to CSV", "title": "PDF to CSV",
"desc": "Extracts Tables from a PDF converting it to CSV" "desc": "Extracts Tables from a PDF converting it to CSV"
}, },
"split-by-size-or-count": { "split-by-size-or-count": {
"tags": "auto,split,size",
"title": "Auto Split by Size/Count", "title": "Auto Split by Size/Count",
"desc": "Split a single PDF into multiple documents based on size, page count, or document count" "desc": "Split a single PDF into multiple documents based on size, page count, or document count"
}, },
"overlay-pdfs": { "overlay-pdfs": {
"tags": "overlay,combine,stack",
"title": "Overlay PDFs", "title": "Overlay PDFs",
"desc": "Overlays PDFs on-top of another PDF" "desc": "Overlays PDFs on-top of another PDF"
}, },
"split-by-sections": { "split-by-sections": {
"tags": "split,sections,divide",
"title": "Split PDF by Sections", "title": "Split PDF by Sections",
"desc": "Divide each page of a PDF into smaller horizontal and vertical sections" "desc": "Divide each page of a PDF into smaller horizontal and vertical sections"
}, },
"AddStampRequest": { "AddStampRequest": {
"tags": "stamp,mark,seal",
"title": "Add Stamp to PDF", "title": "Add Stamp to PDF",
"desc": "Add text or add image stamps at set locations" "desc": "Add text or add image stamps at set locations"
}, },
"removeImage": { "removeImage": {
"tags": "remove,delete,clean",
"title": "Remove image", "title": "Remove image",
"desc": "Remove image from PDF to reduce file size" "desc": "Remove image from PDF to reduce file size"
}, },
"splitByChapters": { "splitByChapters": {
"tags": "split,chapters,structure",
"title": "Split PDF by Chapters", "title": "Split PDF by Chapters",
"desc": "Split a PDF into multiple files based on its chapter structure." "desc": "Split a PDF into multiple files based on its chapter structure."
}, },
"validateSignature": { "validateSignature": {
"tags": "validate,verify,certificate",
"title": "Validate PDF Signature", "title": "Validate PDF Signature",
"desc": "Verify digital signatures and certificates in PDF documents" "desc": "Verify digital signatures and certificates in PDF documents"
}, },
"swagger": { "swagger": {
"tags": "API,documentation,test",
"title": "API Documentation", "title": "API Documentation",
"desc": "View API documentation and test endpoints" "desc": "View API documentation and test endpoints"
}, },
"replace-color": { "replace-color": {
"tags": "color,replace,invert",
"title": "Replace and Invert Color", "title": "Replace and Invert Color",
"desc": "Replace color for text and background in PDF and invert full color of pdf to reduce file size" "desc": "Replace color for text and background in PDF and invert full color of pdf to reduce file size"
} }
@ -1064,7 +1127,6 @@
"info": "Python is not installed. It is required to run." "info": "Python is not installed. It is required to run."
}, },
"sign": { "sign": {
"tags": "authorize,initials,drawn-signature,text-sign,image-signature",
"title": "Sign", "title": "Sign",
"header": "Sign PDFs", "header": "Sign PDFs",
"upload": "Upload Image", "upload": "Upload Image",

View File

@ -9,13 +9,22 @@ import NoToolsFound from './shared/NoToolsFound';
import "./toolPicker/ToolPicker.css"; import "./toolPicker/ToolPicker.css";
interface SearchResultsProps { interface SearchResultsProps {
filteredTools: [string, ToolRegistryEntry][]; filteredTools: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string }>;
onSelect: (id: string) => void; onSelect: (id: string) => void;
searchQuery?: string;
} }
const SearchResults: React.FC<SearchResultsProps> = ({ filteredTools, onSelect }) => { const SearchResults: React.FC<SearchResultsProps> = ({ filteredTools, onSelect, searchQuery }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { searchGroups } = useToolSections(filteredTools); const { searchGroups } = useToolSections(filteredTools, searchQuery);
// Create a map of matched text for quick lookup
const matchedTextMap = new Map<string, string>();
if (filteredTools && Array.isArray(filteredTools)) {
filteredTools.forEach(({ item: [id], matchedText }) => {
if (matchedText) matchedTextMap.set(id, matchedText);
});
}
if (searchGroups.length === 0) { if (searchGroups.length === 0) {
return <NoToolsFound />; return <NoToolsFound />;
@ -28,15 +37,27 @@ const SearchResults: React.FC<SearchResultsProps> = ({ filteredTools, onSelect }
<Box key={group.subcategoryId} w="100%"> <Box key={group.subcategoryId} w="100%">
<SubcategoryHeader label={getSubcategoryLabel(t, group.subcategoryId)} /> <SubcategoryHeader label={getSubcategoryLabel(t, group.subcategoryId)} />
<Stack gap="xs"> <Stack gap="xs">
{group.tools.map(({ id, tool }) => ( {group.tools.map(({ id, tool }) => {
<ToolButton const matchedText = matchedTextMap.get(id);
key={id} // Check if the match was from synonyms and show the actual synonym that matched
id={id} const isSynonymMatch = matchedText && tool.synonyms?.some(synonym =>
tool={tool} matchedText.toLowerCase().includes(synonym.toLowerCase())
isSelected={false} );
onSelect={onSelect} const matchedSynonym = isSynonymMatch ? tool.synonyms?.find(synonym =>
/> matchedText.toLowerCase().includes(synonym.toLowerCase())
))} ) : undefined;
return (
<ToolButton
key={id}
id={id}
tool={tool}
isSelected={false}
onSelect={onSelect}
matchedSynonym={matchedSynonym}
/>
);
})}
</Stack> </Stack>
</Box> </Box>
))} ))}

View File

@ -72,6 +72,7 @@ export default function ToolPanel() {
<SearchResults <SearchResults
filteredTools={filteredTools} filteredTools={filteredTools}
onSelect={handleToolSelect} onSelect={handleToolSelect}
searchQuery={searchQuery}
/> />
</div> </div>
) : leftPanelView === 'toolPicker' ? ( ) : leftPanelView === 'toolPicker' ? (

View File

@ -10,7 +10,7 @@ import { renderToolButtons } from "./shared/renderToolButtons";
interface ToolPickerProps { interface ToolPickerProps {
selectedToolKey: string | null; selectedToolKey: string | null;
onSelect: (id: string) => void; onSelect: (id: string) => void;
filteredTools: [string, ToolRegistryEntry][]; filteredTools: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string }>;
isSearching?: boolean; isSearching?: boolean;
} }

View File

@ -58,8 +58,13 @@ export default function ToolSelector({
return registry; return registry;
}, [baseFilteredTools]); }, [baseFilteredTools]);
// Transform filteredTools to the expected format for useToolSections
const transformedFilteredTools = useMemo(() => {
return filteredTools.map(([id, tool]) => ({ item: [id, tool] as [string, ToolRegistryEntry] }));
}, [filteredTools]);
// Use the same tool sections logic as the main ToolPicker // Use the same tool sections logic as the main ToolPicker
const { sections, searchGroups } = useToolSections(filteredTools); const { sections, searchGroups } = useToolSections(transformedFilteredTools);
// Determine what to display: search results or organized sections // Determine what to display: search results or organized sections
const isSearching = searchTerm.trim().length > 0; const isSearching = searchTerm.trim().length > 0;

View File

@ -13,23 +13,39 @@ export const renderToolButtons = (
selectedToolKey: string | null, selectedToolKey: string | null,
onSelect: (id: string) => void, onSelect: (id: string) => void,
showSubcategoryHeader: boolean = true, showSubcategoryHeader: boolean = true,
disableNavigation: boolean = false disableNavigation: boolean = false,
) => ( searchResults?: Array<{ item: [string, any]; matchedText?: string }>
<Box key={subcategory.subcategoryId} w="100%"> ) => {
{showSubcategoryHeader && ( // Create a map of matched text for quick lookup
<SubcategoryHeader label={getSubcategoryLabel(t, subcategory.subcategoryId)} /> const matchedTextMap = new Map<string, string>();
)} if (searchResults) {
<div> searchResults.forEach(({ item: [id], matchedText }) => {
{subcategory.tools.map(({ id, tool }) => ( if (matchedText) matchedTextMap.set(id, matchedText);
<ToolButton });
key={id} }
id={id}
tool={tool} return (
isSelected={selectedToolKey === id} <Box key={subcategory.subcategoryId} w="100%">
onSelect={onSelect} {showSubcategoryHeader && (
disableNavigation={disableNavigation} <SubcategoryHeader label={getSubcategoryLabel(t, subcategory.subcategoryId)} />
/> )}
))} <div>
</div> {subcategory.tools.map(({ id, tool }) => {
</Box> const matchedSynonym = matchedTextMap.get(id);
);
return (
<ToolButton
key={id}
id={id}
tool={tool}
isSelected={selectedToolKey === id}
onSelect={onSelect}
disableNavigation={disableNavigation}
matchedSynonym={matchedSynonym}
/>
);
})}
</div>
</Box>
);
};

View File

@ -13,9 +13,10 @@ interface ToolButtonProps {
onSelect: (id: string) => void; onSelect: (id: string) => void;
rounded?: boolean; rounded?: boolean;
disableNavigation?: boolean; disableNavigation?: boolean;
matchedSynonym?: string;
} }
const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect, disableNavigation = false }) => { const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect, disableNavigation = false, matchedSynonym }) => {
const isUnavailable = !tool.component && !tool.link; const isUnavailable = !tool.component && !tool.link;
const { getToolNavigation } = useToolNavigation(); const { getToolNavigation } = useToolNavigation();
@ -40,13 +41,27 @@ const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect,
const buttonContent = ( const buttonContent = (
<> <>
<div className="tool-button-icon" style={{ color: "var(--tools-text-and-icon-color)", marginRight: "0.5rem", transform: "scale(0.8)", transformOrigin: "center", opacity: isUnavailable ? 0.25 : 1 }}>{tool.icon}</div> <div className="tool-button-icon" style={{ color: "var(--tools-text-and-icon-color)", marginRight: "0.5rem", transform: "scale(0.8)", transformOrigin: "center", opacity: isUnavailable ? 0.25 : 1 }}>{tool.icon}</div>
<FitText <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', flex: 1, overflow: 'visible' }}>
text={tool.name} <FitText
lines={1} text={tool.name}
minimumFontScale={0.8} lines={1}
as="span" minimumFontScale={0.8}
style={{ display: 'inline-block', maxWidth: '100%', opacity: isUnavailable ? 0.25 : 1 }} as="span"
/> style={{ display: 'inline-block', maxWidth: '100%', opacity: isUnavailable ? 0.25 : 1 }}
/>
{matchedSynonym && (
<span style={{
fontSize: '0.75rem',
color: 'var(--mantine-color-dimmed)',
opacity: isUnavailable ? 0.25 : 1,
marginTop: '1px',
overflow: 'visible',
whiteSpace: 'nowrap'
}}>
{matchedSynonym}
</span>
)}
</div>
</> </>
); );
@ -66,7 +81,10 @@ const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect,
fullWidth fullWidth
justify="flex-start" justify="flex-start"
className="tool-button" className="tool-button"
styles={{ root: { borderRadius: 0, color: "var(--tools-text-and-icon-color)" } }} styles={{
root: { borderRadius: 0, color: "var(--tools-text-and-icon-color)", overflow: 'visible' },
label: { overflow: 'visible' }
}}
> >
{buttonContent} {buttonContent}
</Button> </Button>
@ -84,7 +102,10 @@ const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect,
fullWidth fullWidth
justify="flex-start" justify="flex-start"
className="tool-button" className="tool-button"
styles={{ root: { borderRadius: 0, color: "var(--tools-text-and-icon-color)" } }} styles={{
root: { borderRadius: 0, color: "var(--tools-text-and-icon-color)", overflow: 'visible' },
label: { overflow: 'visible' }
}}
> >
{buttonContent} {buttonContent}
</Button> </Button>
@ -99,7 +120,7 @@ const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect,
justify="flex-start" justify="flex-start"
className="tool-button" className="tool-button"
aria-disabled={isUnavailable} aria-disabled={isUnavailable}
styles={{ root: { borderRadius: 0, color: "var(--tools-text-and-icon-color)", cursor: isUnavailable ? 'not-allowed' : undefined } }} styles={{ root: { borderRadius: 0, color: "var(--tools-text-and-icon-color)", cursor: isUnavailable ? 'not-allowed' : undefined, overflow: 'visible' }, label: { overflow: 'visible' } }}
> >
{buttonContent} {buttonContent}
</Button> </Button>

View File

@ -5,6 +5,7 @@ import LocalIcon from '../../shared/LocalIcon';
import { ToolRegistryEntry } from "../../../data/toolsTaxonomy"; import { ToolRegistryEntry } from "../../../data/toolsTaxonomy";
import { TextInput } from "../../shared/TextInput"; import { TextInput } from "../../shared/TextInput";
import "./ToolPicker.css"; import "./ToolPicker.css";
import { rankByFuzzy, idToWords } from "../../../utils/fuzzySearch";
interface ToolSearchProps { interface ToolSearchProps {
value: string; value: string;
@ -38,15 +39,14 @@ const ToolSearch = ({
const filteredTools = useMemo(() => { const filteredTools = useMemo(() => {
if (!value.trim()) return []; if (!value.trim()) return [];
return Object.entries(toolRegistry) const entries = Object.entries(toolRegistry).filter(([id]) => !(mode === "dropdown" && id === selectedToolKey));
.filter(([id, tool]) => { const ranked = rankByFuzzy(entries, value, [
if (mode === "dropdown" && id === selectedToolKey) return false; ([key]) => idToWords(key),
return ( ([, v]) => v.name,
tool.name.toLowerCase().includes(value.toLowerCase()) || tool.description.toLowerCase().includes(value.toLowerCase()) ([, v]) => v.description,
); ([, v]) => v.synonyms?.join(' ') || '',
}) ]).slice(0, 6);
.slice(0, 6) return ranked.map(({ item: [id, tool] }) => ({ id, tool }));
.map(([id, tool]) => ({ id, tool }));
}, [value, toolRegistry, mode, selectedToolKey]); }, [value, toolRegistry, mode, selectedToolKey]);
const handleSearchChange = (searchValue: string) => { const handleSearchChange = (searchValue: string) => {

View File

@ -11,6 +11,7 @@ import { useNavigationActions, useNavigationState } from './NavigationContext';
import { ToolId, isValidToolId } from '../types/toolId'; import { ToolId, isValidToolId } from '../types/toolId';
import { useNavigationUrlSync } from '../hooks/useUrlSync'; import { useNavigationUrlSync } from '../hooks/useUrlSync';
import { getDefaultWorkbench } from '../types/workbench'; import { getDefaultWorkbench } from '../types/workbench';
import { filterToolRegistryByQuery } from '../utils/toolSearch';
// State interface // State interface
interface ToolWorkflowState { interface ToolWorkflowState {
@ -100,7 +101,7 @@ interface ToolWorkflowContextValue extends ToolWorkflowState {
handleReaderToggle: () => void; handleReaderToggle: () => void;
// Computed values // Computed values
filteredTools: [string, ToolRegistryEntry][]; // Filtered by search filteredTools: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string }>; // Filtered by search
isPanelVisible: boolean; isPanelVisible: boolean;
} }
@ -219,12 +220,10 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
setReaderMode(true); setReaderMode(true);
}, [setReaderMode]); }, [setReaderMode]);
// Filter tools based on search query // Filter tools based on search query with fuzzy matching (name, description, id, synonyms)
const filteredTools = useMemo(() => { const filteredTools = useMemo(() => {
if (!toolRegistry) return []; if (!toolRegistry) return [];
return Object.entries(toolRegistry).filter(([_, { name }]) => return filterToolRegistryByQuery(toolRegistry as Record<string, ToolRegistryEntry>, state.searchQuery);
name.toLowerCase().includes(state.searchQuery.toLowerCase())
);
}, [toolRegistry, state.searchQuery]); }, [toolRegistry, state.searchQuery]);
const isPanelVisible = useMemo(() => const isPanelVisible = useMemo(() =>

View File

@ -45,6 +45,8 @@ export type ToolRegistryEntry = {
operationConfig?: ToolOperationConfig<any>; operationConfig?: ToolOperationConfig<any>;
// Settings component for automation configuration // Settings component for automation configuration
settingsComponent?: React.ComponentType<any>; settingsComponent?: React.ComponentType<any>;
// Synonyms for search (optional)
synonyms?: string[];
} }
export type ToolRegistry = Record<ToolId, ToolRegistryEntry>; export type ToolRegistry = Record<ToolId, ToolRegistryEntry>;

View File

@ -12,6 +12,7 @@ import RemoveBlanks from "../tools/RemoveBlanks";
import RemovePages from "../tools/RemovePages"; import RemovePages from "../tools/RemovePages";
import RemovePassword from "../tools/RemovePassword"; import RemovePassword from "../tools/RemovePassword";
import { SubcategoryId, ToolCategoryId, ToolRegistry } from "./toolsTaxonomy"; import { SubcategoryId, ToolCategoryId, ToolRegistry } from "./toolsTaxonomy";
import { getSynonyms } from "../utils/toolSynonyms";
import AddWatermark from "../tools/AddWatermark"; import AddWatermark from "../tools/AddWatermark";
import AddStamp from "../tools/AddStamp"; import AddStamp from "../tools/AddStamp";
import Merge from '../tools/Merge'; import Merge from '../tools/Merge';
@ -172,6 +173,7 @@ export function useFlatToolRegistry(): ToolRegistry {
description: t("home.certSign.desc", "Sign PDF documents using digital certificates"), description: t("home.certSign.desc", "Sign PDF documents using digital certificates"),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.SIGNING, subcategoryId: SubcategoryId.SIGNING,
synonyms: getSynonyms(t, "certSign"),
maxFiles: -1, maxFiles: -1,
endpoints: ["cert-sign"], endpoints: ["cert-sign"],
operationConfig: certSignOperationConfig, operationConfig: certSignOperationConfig,
@ -184,6 +186,7 @@ export function useFlatToolRegistry(): ToolRegistry {
description: t("home.sign.desc", "Adds signature to PDF by drawing, text or image"), description: t("home.sign.desc", "Adds signature to PDF by drawing, text or image"),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.SIGNING, subcategoryId: SubcategoryId.SIGNING,
synonyms: getSynonyms(t, "sign")
}, },
// Document Security // Document Security
@ -199,7 +202,8 @@ export function useFlatToolRegistry(): ToolRegistry {
endpoints: ["add-password"], endpoints: ["add-password"],
operationConfig: addPasswordOperationConfig, operationConfig: addPasswordOperationConfig,
settingsComponent: AddPasswordSettings, settingsComponent: AddPasswordSettings,
}, synonyms: getSynonyms(t, "addPassword")
},
watermark: { watermark: {
icon: <LocalIcon icon="branding-watermark-outline-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="branding-watermark-outline-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.watermark.title", "Add Watermark"), name: t("home.watermark.title", "Add Watermark"),
@ -211,6 +215,7 @@ export function useFlatToolRegistry(): ToolRegistry {
endpoints: ["add-watermark"], endpoints: ["add-watermark"],
operationConfig: addWatermarkOperationConfig, operationConfig: addWatermarkOperationConfig,
settingsComponent: AddWatermarkSingleStepSettings, settingsComponent: AddWatermarkSingleStepSettings,
synonyms: getSynonyms(t, "watermark")
}, },
addStamp: { addStamp: {
icon: <LocalIcon icon="approval-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="approval-rounded" width="1.5rem" height="1.5rem" />,
@ -219,6 +224,7 @@ export function useFlatToolRegistry(): ToolRegistry {
description: t("home.addStamp.desc", "Add text or add image stamps at set locations"), description: t("home.addStamp.desc", "Add text or add image stamps at set locations"),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_SECURITY, subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
synonyms: getSynonyms(t, "addStamp"),
maxFiles: -1, maxFiles: -1,
endpoints: ["add-stamp"], endpoints: ["add-stamp"],
operationConfig: addStampOperationConfig, operationConfig: addStampOperationConfig,
@ -234,6 +240,7 @@ export function useFlatToolRegistry(): ToolRegistry {
endpoints: ["sanitize-pdf"], endpoints: ["sanitize-pdf"],
operationConfig: sanitizeOperationConfig, operationConfig: sanitizeOperationConfig,
settingsComponent: SanitizeSettings, settingsComponent: SanitizeSettings,
synonyms: getSynonyms(t, "sanitize")
}, },
flatten: { flatten: {
icon: <LocalIcon icon="layers-clear-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="layers-clear-rounded" width="1.5rem" height="1.5rem" />,
@ -246,6 +253,7 @@ export function useFlatToolRegistry(): ToolRegistry {
endpoints: ["flatten"], endpoints: ["flatten"],
operationConfig: flattenOperationConfig, operationConfig: flattenOperationConfig,
settingsComponent: FlattenSettings, settingsComponent: FlattenSettings,
synonyms: getSynonyms(t, "flatten")
}, },
unlockPDFForms: { unlockPDFForms: {
icon: <LocalIcon icon="preview-off-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="preview-off-rounded" width="1.5rem" height="1.5rem" />,
@ -258,6 +266,7 @@ export function useFlatToolRegistry(): ToolRegistry {
endpoints: ["unlock-pdf-forms"], endpoints: ["unlock-pdf-forms"],
operationConfig: unlockPdfFormsOperationConfig, operationConfig: unlockPdfFormsOperationConfig,
settingsComponent: UnlockPdfFormsSettings, settingsComponent: UnlockPdfFormsSettings,
synonyms: getSynonyms(t, "unlockPDFForms"),
}, },
manageCertificates: { manageCertificates: {
icon: <LocalIcon icon="license-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="license-rounded" width="1.5rem" height="1.5rem" />,
@ -269,6 +278,7 @@ export function useFlatToolRegistry(): ToolRegistry {
), ),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_SECURITY, subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
synonyms: getSynonyms(t, "manageCertificates"),
}, },
changePermissions: { changePermissions: {
icon: <LocalIcon icon="lock-outline" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="lock-outline" width="1.5rem" height="1.5rem" />,
@ -281,6 +291,7 @@ export function useFlatToolRegistry(): ToolRegistry {
endpoints: ["add-password"], endpoints: ["add-password"],
operationConfig: changePermissionsOperationConfig, operationConfig: changePermissionsOperationConfig,
settingsComponent: ChangePermissionsSettings, settingsComponent: ChangePermissionsSettings,
synonyms: getSynonyms(t, "changePermissions"),
}, },
getPdfInfo: { getPdfInfo: {
icon: <LocalIcon icon="fact-check-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="fact-check-rounded" width="1.5rem" height="1.5rem" />,
@ -289,6 +300,7 @@ export function useFlatToolRegistry(): ToolRegistry {
description: t("home.getPdfInfo.desc", "Grabs any and all information possible on PDFs"), description: t("home.getPdfInfo.desc", "Grabs any and all information possible on PDFs"),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.VERIFICATION, subcategoryId: SubcategoryId.VERIFICATION,
synonyms: getSynonyms(t, "getPdfInfo"),
}, },
validateSignature: { validateSignature: {
icon: <LocalIcon icon="verified-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="verified-rounded" width="1.5rem" height="1.5rem" />,
@ -297,11 +309,12 @@ export function useFlatToolRegistry(): ToolRegistry {
description: t("home.validateSignature.desc", "Verify digital signatures and certificates in PDF documents"), description: t("home.validateSignature.desc", "Verify digital signatures and certificates in PDF documents"),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.VERIFICATION, subcategoryId: SubcategoryId.VERIFICATION,
synonyms: getSynonyms(t, "validateSignature"),
}, },
// Document Review // Document Review
read: { read: {
icon: <LocalIcon icon="article-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="article-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.read.title", "Read"), name: t("home.read.title", "Read"),
component: null, component: null,
@ -312,6 +325,7 @@ export function useFlatToolRegistry(): ToolRegistry {
), ),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_REVIEW, subcategoryId: SubcategoryId.DOCUMENT_REVIEW,
synonyms: getSynonyms(t, "read")
}, },
changeMetadata: { changeMetadata: {
icon: <LocalIcon icon="assignment-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="assignment-rounded" width="1.5rem" height="1.5rem" />,
@ -324,6 +338,7 @@ export function useFlatToolRegistry(): ToolRegistry {
endpoints: ["update-metadata"], endpoints: ["update-metadata"],
operationConfig: changeMetadataOperationConfig, operationConfig: changeMetadataOperationConfig,
settingsComponent: ChangeMetadataSingleStep, settingsComponent: ChangeMetadataSingleStep,
synonyms: getSynonyms(t, "changeMetadata")
}, },
// Page Formatting // Page Formatting
@ -350,6 +365,7 @@ export function useFlatToolRegistry(): ToolRegistry {
endpoints: ["rotate-pdf"], endpoints: ["rotate-pdf"],
operationConfig: rotateOperationConfig, operationConfig: rotateOperationConfig,
settingsComponent: RotateSettings, settingsComponent: RotateSettings,
synonyms: getSynonyms(t, "rotate")
}, },
split: { split: {
icon: <LocalIcon icon="content-cut-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="content-cut-rounded" width="1.5rem" height="1.5rem" />,
@ -360,6 +376,7 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.PAGE_FORMATTING, subcategoryId: SubcategoryId.PAGE_FORMATTING,
operationConfig: splitOperationConfig, operationConfig: splitOperationConfig,
settingsComponent: SplitSettings, settingsComponent: SplitSettings,
synonyms: getSynonyms(t, "split")
}, },
reorganizePages: { reorganizePages: {
icon: <LocalIcon icon="move-down-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="move-down-rounded" width="1.5rem" height="1.5rem" />,
@ -372,6 +389,7 @@ export function useFlatToolRegistry(): ToolRegistry {
), ),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING, subcategoryId: SubcategoryId.PAGE_FORMATTING,
synonyms: getSynonyms(t, "reorganizePages")
}, },
scalePages: { scalePages: {
icon: <LocalIcon icon="crop-free-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="crop-free-rounded" width="1.5rem" height="1.5rem" />,
@ -384,6 +402,7 @@ export function useFlatToolRegistry(): ToolRegistry {
endpoints: ["scale-pages"], endpoints: ["scale-pages"],
operationConfig: adjustPageScaleOperationConfig, operationConfig: adjustPageScaleOperationConfig,
settingsComponent: AdjustPageScaleSettings, settingsComponent: AdjustPageScaleSettings,
synonyms: getSynonyms(t, "scalePages")
}, },
addPageNumbers: { addPageNumbers: {
icon: <LocalIcon icon="123-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="123-rounded" width="1.5rem" height="1.5rem" />,
@ -393,6 +412,7 @@ export function useFlatToolRegistry(): ToolRegistry {
description: t("home.addPageNumbers.desc", "Add Page numbers throughout a document in a set location"), description: t("home.addPageNumbers.desc", "Add Page numbers throughout a document in a set location"),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING, subcategoryId: SubcategoryId.PAGE_FORMATTING,
synonyms: getSynonyms(t, "addPageNumbers")
}, },
pageLayout: { pageLayout: {
icon: <LocalIcon icon="dashboard-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="dashboard-rounded" width="1.5rem" height="1.5rem" />,
@ -402,6 +422,7 @@ export function useFlatToolRegistry(): ToolRegistry {
description: t("home.pageLayout.desc", "Merge multiple pages of a PDF document into a single page"), description: t("home.pageLayout.desc", "Merge multiple pages of a PDF document into a single page"),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING, subcategoryId: SubcategoryId.PAGE_FORMATTING,
synonyms: getSynonyms(t, "pageLayout")
}, },
bookletImposition: { bookletImposition: {
icon: <LocalIcon icon="menu-book-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="menu-book-rounded" width="1.5rem" height="1.5rem" />,
@ -426,6 +447,7 @@ export function useFlatToolRegistry(): ToolRegistry {
urlPath: '/pdf-to-single-page', urlPath: '/pdf-to-single-page',
endpoints: ["pdf-to-single-page"], endpoints: ["pdf-to-single-page"],
operationConfig: singleLargePageOperationConfig, operationConfig: singleLargePageOperationConfig,
synonyms: getSynonyms(t, "pdfToSinglePage")
}, },
addAttachments: { addAttachments: {
icon: <LocalIcon icon="attachment-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="attachment-rounded" width="1.5rem" height="1.5rem" />,
@ -435,6 +457,7 @@ export function useFlatToolRegistry(): ToolRegistry {
description: t("home.addAttachments.desc", "Add or remove embedded files (attachments) to/from a PDF"), description: t("home.addAttachments.desc", "Add or remove embedded files (attachments) to/from a PDF"),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING, subcategoryId: SubcategoryId.PAGE_FORMATTING,
synonyms: getSynonyms(t, "addAttachments")
}, },
// Extraction // Extraction
@ -446,6 +469,7 @@ export function useFlatToolRegistry(): ToolRegistry {
description: t("home.extractPages.desc", "Extract specific pages from a PDF document"), description: t("home.extractPages.desc", "Extract specific pages from a PDF document"),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.EXTRACTION, subcategoryId: SubcategoryId.EXTRACTION,
synonyms: getSynonyms(t, "extractPages")
}, },
extractImages: { extractImages: {
icon: <LocalIcon icon="filter-alt" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="filter-alt" width="1.5rem" height="1.5rem" />,
@ -454,6 +478,7 @@ export function useFlatToolRegistry(): ToolRegistry {
description: t("home.extractImages.desc", "Extract images from PDF documents"), description: t("home.extractImages.desc", "Extract images from PDF documents"),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.EXTRACTION, subcategoryId: SubcategoryId.EXTRACTION,
synonyms: getSynonyms(t, "extractImages")
}, },
// Removal // Removal
@ -467,6 +492,7 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.REMOVAL, subcategoryId: SubcategoryId.REMOVAL,
maxFiles: 1, maxFiles: 1,
endpoints: ["remove-pages"], endpoints: ["remove-pages"],
synonyms: getSynonyms(t, "removePages")
}, },
removeBlanks: { removeBlanks: {
icon: <LocalIcon icon="scan-delete-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="scan-delete-rounded" width="1.5rem" height="1.5rem" />,
@ -477,6 +503,7 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategoryId: SubcategoryId.REMOVAL, subcategoryId: SubcategoryId.REMOVAL,
maxFiles: 1, maxFiles: 1,
endpoints: ["remove-blanks"], endpoints: ["remove-blanks"],
synonyms: getSynonyms(t, "removeBlanks")
}, },
removeAnnotations: { removeAnnotations: {
icon: <LocalIcon icon="thread-unread-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="thread-unread-rounded" width="1.5rem" height="1.5rem" />,
@ -485,6 +512,7 @@ export function useFlatToolRegistry(): ToolRegistry {
description: t("home.removeAnnotations.desc", "Remove annotations and comments from PDF documents"), description: t("home.removeAnnotations.desc", "Remove annotations and comments from PDF documents"),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.REMOVAL, subcategoryId: SubcategoryId.REMOVAL,
synonyms: getSynonyms(t, "removeAnnotations")
}, },
removeImage: { removeImage: {
icon: <LocalIcon icon="remove-selection-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="remove-selection-rounded" width="1.5rem" height="1.5rem" />,
@ -493,6 +521,7 @@ export function useFlatToolRegistry(): ToolRegistry {
description: t("home.removeImage.desc", "Remove images from PDF documents"), description: t("home.removeImage.desc", "Remove images from PDF documents"),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.REMOVAL, subcategoryId: SubcategoryId.REMOVAL,
synonyms: getSynonyms(t, "removeImage"),
}, },
removePassword: { removePassword: {
icon: <LocalIcon icon="lock-open-right-outline-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="lock-open-right-outline-rounded" width="1.5rem" height="1.5rem" />,
@ -505,6 +534,7 @@ export function useFlatToolRegistry(): ToolRegistry {
maxFiles: -1, maxFiles: -1,
operationConfig: removePasswordOperationConfig, operationConfig: removePasswordOperationConfig,
settingsComponent: RemovePasswordSettings, settingsComponent: RemovePasswordSettings,
synonyms: getSynonyms(t, "removePassword")
}, },
removeCertSign: { removeCertSign: {
icon: <LocalIcon icon="remove-moderator-outline-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="remove-moderator-outline-rounded" width="1.5rem" height="1.5rem" />,
@ -516,6 +546,7 @@ export function useFlatToolRegistry(): ToolRegistry {
maxFiles: -1, maxFiles: -1,
endpoints: ["remove-certificate-sign"], endpoints: ["remove-certificate-sign"],
operationConfig: removeCertificateSignOperationConfig, operationConfig: removeCertificateSignOperationConfig,
synonyms: getSynonyms(t, "removeCertSign"),
}, },
// Automation // Automation
@ -533,6 +564,7 @@ export function useFlatToolRegistry(): ToolRegistry {
maxFiles: -1, maxFiles: -1,
supportedFormats: CONVERT_SUPPORTED_FORMATS, supportedFormats: CONVERT_SUPPORTED_FORMATS,
endpoints: ["handleData"], endpoints: ["handleData"],
synonyms: getSynonyms(t, "automate"),
}, },
autoRename: { autoRename: {
icon: <LocalIcon icon="match-word-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="match-word-rounded" width="1.5rem" height="1.5rem" />,
@ -544,6 +576,7 @@ export function useFlatToolRegistry(): ToolRegistry {
description: t("home.autoRename.desc", "Automatically rename PDF files based on their content"), description: t("home.autoRename.desc", "Automatically rename PDF files based on their content"),
categoryId: ToolCategoryId.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.AUTOMATION, subcategoryId: SubcategoryId.AUTOMATION,
synonyms: getSynonyms(t, "autoRename"),
}, },
autoSplitPDF: { autoSplitPDF: {
icon: <LocalIcon icon="split-scene-right-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="split-scene-right-rounded" width="1.5rem" height="1.5rem" />,
@ -552,6 +585,7 @@ export function useFlatToolRegistry(): ToolRegistry {
description: t("home.autoSplitPDF.desc", "Automatically split PDF pages based on content detection"), description: t("home.autoSplitPDF.desc", "Automatically split PDF pages based on content detection"),
categoryId: ToolCategoryId.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.AUTOMATION, subcategoryId: SubcategoryId.AUTOMATION,
synonyms: getSynonyms(t, "autoSplitPDF"),
}, },
autoSizeSplitPDF: { autoSizeSplitPDF: {
icon: <LocalIcon icon="content-cut-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="content-cut-rounded" width="1.5rem" height="1.5rem" />,
@ -560,6 +594,7 @@ export function useFlatToolRegistry(): ToolRegistry {
description: t("home.autoSizeSplitPDF.desc", "Automatically split PDFs by file size or page count"), description: t("home.autoSizeSplitPDF.desc", "Automatically split PDFs by file size or page count"),
categoryId: ToolCategoryId.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.AUTOMATION, subcategoryId: SubcategoryId.AUTOMATION,
synonyms: getSynonyms(t, "autoSizeSplitPDF"),
}, },
// Advanced Formatting // Advanced Formatting
@ -571,6 +606,7 @@ export function useFlatToolRegistry(): ToolRegistry {
description: t("home.adjustContrast.desc", "Adjust colors and contrast of PDF documents"), description: t("home.adjustContrast.desc", "Adjust colors and contrast of PDF documents"),
categoryId: ToolCategoryId.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING, subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
synonyms: getSynonyms(t, "adjustContrast"),
}, },
repair: { repair: {
icon: <LocalIcon icon="build-outline-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="build-outline-rounded" width="1.5rem" height="1.5rem" />,
@ -583,6 +619,7 @@ export function useFlatToolRegistry(): ToolRegistry {
endpoints: ["repair"], endpoints: ["repair"],
operationConfig: repairOperationConfig, operationConfig: repairOperationConfig,
settingsComponent: RepairSettings, settingsComponent: RepairSettings,
synonyms: getSynonyms(t, "repair")
}, },
scannerImageSplit: { scannerImageSplit: {
icon: <LocalIcon icon="scanner-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="scanner-rounded" width="1.5rem" height="1.5rem" />,
@ -591,6 +628,7 @@ export function useFlatToolRegistry(): ToolRegistry {
description: t("home.scannerImageSplit.desc", "Detect and split scanned photos into separate pages"), description: t("home.scannerImageSplit.desc", "Detect and split scanned photos into separate pages"),
categoryId: ToolCategoryId.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING, subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
synonyms: getSynonyms(t, "ScannerImageSplit"),
}, },
overlayPdfs: { overlayPdfs: {
icon: <LocalIcon icon="layers-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="layers-rounded" width="1.5rem" height="1.5rem" />,
@ -599,6 +637,7 @@ export function useFlatToolRegistry(): ToolRegistry {
description: t("home.overlayPdfs.desc", "Overlay one PDF on top of another"), description: t("home.overlayPdfs.desc", "Overlay one PDF on top of another"),
categoryId: ToolCategoryId.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING, subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
synonyms: getSynonyms(t, "overlayPdfs"),
}, },
replaceColorPdf: { replaceColorPdf: {
icon: <LocalIcon icon="format-color-fill-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="format-color-fill-rounded" width="1.5rem" height="1.5rem" />,
@ -607,6 +646,7 @@ export function useFlatToolRegistry(): ToolRegistry {
description: t("home.replaceColorPdf.desc", "Replace or invert colors in PDF documents"), description: t("home.replaceColorPdf.desc", "Replace or invert colors in PDF documents"),
categoryId: ToolCategoryId.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING, subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
synonyms: getSynonyms(t, "replaceColorPdf"),
}, },
addImage: { addImage: {
icon: <LocalIcon icon="image-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="image-rounded" width="1.5rem" height="1.5rem" />,
@ -615,6 +655,7 @@ export function useFlatToolRegistry(): ToolRegistry {
description: t("home.addImage.desc", "Add images to PDF documents"), description: t("home.addImage.desc", "Add images to PDF documents"),
categoryId: ToolCategoryId.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING, subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
synonyms: getSynonyms(t, "addImage"),
}, },
editTableOfContents: { editTableOfContents: {
icon: <LocalIcon icon="bookmark-add-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="bookmark-add-rounded" width="1.5rem" height="1.5rem" />,
@ -623,6 +664,7 @@ export function useFlatToolRegistry(): ToolRegistry {
description: t("home.editTableOfContents.desc", "Add or edit bookmarks and table of contents in PDF documents"), description: t("home.editTableOfContents.desc", "Add or edit bookmarks and table of contents in PDF documents"),
categoryId: ToolCategoryId.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING, subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
synonyms: getSynonyms(t, "editTableOfContents"),
}, },
fakeScan: { fakeScan: {
icon: <LocalIcon icon="scanner-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="scanner-rounded" width="1.5rem" height="1.5rem" />,
@ -631,6 +673,7 @@ export function useFlatToolRegistry(): ToolRegistry {
description: t("home.fakeScan.desc", "Create a PDF that looks like it was scanned"), description: t("home.fakeScan.desc", "Create a PDF that looks like it was scanned"),
categoryId: ToolCategoryId.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING, subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
synonyms: getSynonyms(t, "fakeScan"),
}, },
// Developer Tools // Developer Tools
@ -642,6 +685,7 @@ export function useFlatToolRegistry(): ToolRegistry {
description: t("home.showJS.desc", "Extract and display JavaScript code from PDF documents"), description: t("home.showJS.desc", "Extract and display JavaScript code from PDF documents"),
categoryId: ToolCategoryId.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.DEVELOPER_TOOLS, subcategoryId: SubcategoryId.DEVELOPER_TOOLS,
synonyms: getSynonyms(t, "showJS"),
}, },
devApi: { devApi: {
icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: "#2F7BF6" }} />, icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: "#2F7BF6" }} />,
@ -651,6 +695,7 @@ export function useFlatToolRegistry(): ToolRegistry {
categoryId: ToolCategoryId.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.DEVELOPER_TOOLS, subcategoryId: SubcategoryId.DEVELOPER_TOOLS,
link: "https://stirlingpdf.io/swagger-ui/5.21.0/index.html", link: "https://stirlingpdf.io/swagger-ui/5.21.0/index.html",
synonyms: getSynonyms(t, "devApi"),
}, },
devFolderScanning: { devFolderScanning: {
icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: "#2F7BF6" }} />, icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: "#2F7BF6" }} />,
@ -660,6 +705,7 @@ export function useFlatToolRegistry(): ToolRegistry {
categoryId: ToolCategoryId.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.DEVELOPER_TOOLS, subcategoryId: SubcategoryId.DEVELOPER_TOOLS,
link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Folder%20Scanning/", link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Folder%20Scanning/",
synonyms: getSynonyms(t, "devFolderScanning"),
}, },
devSsoGuide: { devSsoGuide: {
icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: "#2F7BF6" }} />, icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: "#2F7BF6" }} />,
@ -669,6 +715,7 @@ export function useFlatToolRegistry(): ToolRegistry {
categoryId: ToolCategoryId.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.DEVELOPER_TOOLS, subcategoryId: SubcategoryId.DEVELOPER_TOOLS,
link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Single%20Sign-On%20Configuration", link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Single%20Sign-On%20Configuration",
synonyms: getSynonyms(t, "devSsoGuide"),
}, },
devAirgapped: { devAirgapped: {
icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: "#2F7BF6" }} />, icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: "#2F7BF6" }} />,
@ -678,6 +725,7 @@ export function useFlatToolRegistry(): ToolRegistry {
categoryId: ToolCategoryId.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.DEVELOPER_TOOLS, subcategoryId: SubcategoryId.DEVELOPER_TOOLS,
link: "https://docs.stirlingpdf.com/Pro/#activation", link: "https://docs.stirlingpdf.com/Pro/#activation",
synonyms: getSynonyms(t, "devAirgapped"),
}, },
// Recommended Tools // Recommended Tools
@ -688,6 +736,7 @@ export function useFlatToolRegistry(): ToolRegistry {
description: t("home.compare.desc", "Compare two PDF documents and highlight differences"), description: t("home.compare.desc", "Compare two PDF documents and highlight differences"),
categoryId: ToolCategoryId.RECOMMENDED_TOOLS, categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategoryId: SubcategoryId.GENERAL, subcategoryId: SubcategoryId.GENERAL,
synonyms: getSynonyms(t, "compare")
}, },
compress: { compress: {
icon: <LocalIcon icon="zoom-in-map-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="zoom-in-map-rounded" width="1.5rem" height="1.5rem" />,
@ -699,6 +748,7 @@ export function useFlatToolRegistry(): ToolRegistry {
maxFiles: -1, maxFiles: -1,
operationConfig: compressOperationConfig, operationConfig: compressOperationConfig,
settingsComponent: CompressSettings, settingsComponent: CompressSettings,
synonyms: getSynonyms(t, "compress")
}, },
convert: { convert: {
icon: <LocalIcon icon="sync-alt-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="sync-alt-rounded" width="1.5rem" height="1.5rem" />,
@ -728,6 +778,7 @@ export function useFlatToolRegistry(): ToolRegistry {
operationConfig: convertOperationConfig, operationConfig: convertOperationConfig,
settingsComponent: ConvertSettings, settingsComponent: ConvertSettings,
synonyms: getSynonyms(t, "convert")
}, },
merge: { merge: {
icon: <LocalIcon icon="library-add-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="library-add-rounded" width="1.5rem" height="1.5rem" />,
@ -739,7 +790,8 @@ export function useFlatToolRegistry(): ToolRegistry {
maxFiles: -1, maxFiles: -1,
endpoints: ["merge-pdfs"], endpoints: ["merge-pdfs"],
operationConfig: mergeOperationConfig, operationConfig: mergeOperationConfig,
settingsComponent: MergeSettings settingsComponent: MergeSettings,
synonyms: getSynonyms(t, "merge")
}, },
multiTool: { multiTool: {
icon: <LocalIcon icon="dashboard-customize-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="dashboard-customize-rounded" width="1.5rem" height="1.5rem" />,
@ -750,6 +802,7 @@ export function useFlatToolRegistry(): ToolRegistry {
categoryId: ToolCategoryId.RECOMMENDED_TOOLS, categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategoryId: SubcategoryId.GENERAL, subcategoryId: SubcategoryId.GENERAL,
maxFiles: -1, maxFiles: -1,
synonyms: getSynonyms(t, "multiTool"),
}, },
ocr: { ocr: {
icon: <LocalIcon icon="quick-reference-all-outline-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="quick-reference-all-outline-rounded" width="1.5rem" height="1.5rem" />,
@ -762,6 +815,7 @@ export function useFlatToolRegistry(): ToolRegistry {
urlPath: '/ocr-pdf', urlPath: '/ocr-pdf',
operationConfig: ocrOperationConfig, operationConfig: ocrOperationConfig,
settingsComponent: OCRSettings, settingsComponent: OCRSettings,
synonyms: getSynonyms(t, "ocr")
}, },
redact: { redact: {
icon: <LocalIcon icon="visibility-off-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="visibility-off-rounded" width="1.5rem" height="1.5rem" />,
@ -774,6 +828,7 @@ export function useFlatToolRegistry(): ToolRegistry {
endpoints: ["auto-redact"], endpoints: ["auto-redact"],
operationConfig: redactOperationConfig, operationConfig: redactOperationConfig,
settingsComponent: RedactSingleStepSettings, settingsComponent: RedactSingleStepSettings,
synonyms: getSynonyms(t, "redact")
}, },
}; };

View File

@ -27,12 +27,19 @@ export interface ToolSection {
subcategories: SubcategoryGroup[]; subcategories: SubcategoryGroup[];
}; };
export function useToolSections(filteredTools: [string /* FIX ME: Should be ToolId */, ToolRegistryEntry][]) { export function useToolSections(
filteredTools: Array<{ item: [string /* FIX ME: Should be ToolId */, ToolRegistryEntry]; matchedText?: string }>,
searchQuery?: string
) {
const { t } = useTranslation(); const { t } = useTranslation();
const groupedTools = useMemo(() => { const groupedTools = useMemo(() => {
if (!filteredTools || !Array.isArray(filteredTools)) {
return {} as GroupedTools;
}
const grouped = {} as GroupedTools; const grouped = {} as GroupedTools;
filteredTools.forEach(([id, tool]) => { filteredTools.forEach(({ item: [id, tool] }) => {
const categoryId = tool.categoryId; const categoryId = tool.categoryId;
const subcategoryId = tool.subcategoryId; const subcategoryId = tool.subcategoryId;
if (!grouped[categoryId]) grouped[categoryId] = {} as SubcategoryIdMap; if (!grouped[categoryId]) grouped[categoryId] = {} as SubcategoryIdMap;
@ -92,9 +99,13 @@ export function useToolSections(filteredTools: [string /* FIX ME: Should be Tool
}, [groupedTools]); }, [groupedTools]);
const searchGroups: SubcategoryGroup[] = useMemo(() => { const searchGroups: SubcategoryGroup[] = useMemo(() => {
if (!filteredTools || !Array.isArray(filteredTools)) {
return [];
}
const subMap = {} as SubcategoryIdMap; const subMap = {} as SubcategoryIdMap;
const seen = new Set<string /* FIX ME: Should be ToolId */>(); const seen = new Set<string /* FIX ME: Should be ToolId */>();
filteredTools.forEach(([id, tool]) => { filteredTools.forEach(({ item: [id, tool] }) => {
const toolId = id as string /* FIX ME: Should be ToolId */; const toolId = id as string /* FIX ME: Should be ToolId */;
if (seen.has(toolId)) return; if (seen.has(toolId)) return;
seen.add(toolId); seen.add(toolId);
@ -102,10 +113,31 @@ export function useToolSections(filteredTools: [string /* FIX ME: Should be Tool
if (!subMap[sub]) subMap[sub] = []; if (!subMap[sub]) subMap[sub] = [];
subMap[sub].push({ id: toolId, tool }); subMap[sub].push({ id: toolId, tool });
}); });
return Object.entries(subMap) const entries = Object.entries(subMap);
// If a search query is present, always order subcategories by first occurrence in
// the ranked filteredTools list so the top-ranked tools' subcategory appears first.
if (searchQuery && searchQuery.trim()) {
const order: SubcategoryId[] = [];
filteredTools.forEach(({ item: [_, tool] }) => {
const sc = tool.subcategoryId;
if (!order.includes(sc)) order.push(sc);
});
return entries
.sort(([a], [b]) => {
const ai = order.indexOf(a as SubcategoryId);
const bi = order.indexOf(b as SubcategoryId);
if (ai !== bi) return ai - bi;
return (a as SubcategoryId).localeCompare(b as SubcategoryId);
})
.map(([subcategoryId, tools]) => ({ subcategoryId, tools } as SubcategoryGroup));
}
// No search: alphabetical subcategory ordering
return entries
.sort(([a], [b]) => a.localeCompare(b)) .sort(([a], [b]) => a.localeCompare(b))
.map(([subcategoryId, tools]) => ({ subcategoryId, tools } as SubcategoryGroup)); .map(([subcategoryId, tools]) => ({ subcategoryId, tools } as SubcategoryGroup));
}, [filteredTools]); }, [filteredTools, searchQuery]);
return { sections, searchGroups }; return { sections, searchGroups };
} }

View File

@ -0,0 +1,121 @@
// Lightweight fuzzy search helpers without external deps
// Provides diacritics-insensitive normalization and Levenshtein distance scoring
function normalizeText(text: string): string {
return text
.toLowerCase()
.normalize('NFD')
.replace(/\p{Diacritic}+/gu, '')
.trim();
}
// Basic Levenshtein distance (iterative with two rows)
function levenshtein(a: string, b: string): number {
if (a === b) return 0;
const aLen = a.length;
const bLen = b.length;
if (aLen === 0) return bLen;
if (bLen === 0) return aLen;
const prev = new Array(bLen + 1);
const curr = new Array(bLen + 1);
for (let j = 0; j <= bLen; j++) prev[j] = j;
for (let i = 1; i <= aLen; i++) {
curr[0] = i;
const aChar = a.charCodeAt(i - 1);
for (let j = 1; j <= bLen; j++) {
const cost = aChar === b.charCodeAt(j - 1) ? 0 : 1;
curr[j] = Math.min(
prev[j] + 1, // deletion
curr[j - 1] + 1, // insertion
prev[j - 1] + cost // substitution
);
}
for (let j = 0; j <= bLen; j++) prev[j] = curr[j];
}
return curr[bLen];
}
// Compute a heuristic match score (higher is better)
// 1) Exact/substring hits get high base; 2) otherwise use normalized Levenshtein distance
export function scoreMatch(queryRaw: string, targetRaw: string): number {
const query = normalizeText(queryRaw);
const target = normalizeText(targetRaw);
if (!query) return 0;
if (target.includes(query)) {
// Reward earlier/shorter substring matches
const pos = target.indexOf(query);
return 100 - pos - Math.max(0, target.length - query.length);
}
// Token-aware: check each word token too, but require better similarity
const tokens = target.split(/[^a-z0-9]+/g).filter(Boolean);
for (const token of tokens) {
if (token.includes(query)) {
// Only give high score if the match is substantial (not just "and" matching)
const similarity = query.length / Math.max(query.length, token.length);
if (similarity >= 0.6) { // Require at least 60% similarity
return 80 - Math.abs(token.length - query.length);
}
}
}
const distance = levenshtein(query, target.length > 64 ? target.slice(0, 64) : target);
const maxLen = Math.max(query.length, target.length, 1);
const similarity = 1 - distance / maxLen; // 0..1
return Math.floor(similarity * 60); // scale below substring scores
}
export function minScoreForQuery(query: string): number {
const len = normalizeText(query).length;
if (len <= 3) return 40;
if (len <= 6) return 30;
return 25;
}
// Decide if a target matches a query based on a threshold
export function isFuzzyMatch(query: string, target: string, minScore?: number): boolean {
const threshold = typeof minScore === 'number' ? minScore : minScoreForQuery(query);
return scoreMatch(query, target) >= threshold;
}
// Convenience: rank a list of items by best score across provided getters
export function rankByFuzzy<T>(items: T[], query: string, getters: Array<(item: T) => string>, minScore?: number): Array<{ item: T; score: number; matchedText?: string }>{
const results: Array<{ item: T; score: number; matchedText?: string }> = [];
const threshold = typeof minScore === 'number' ? minScore : minScoreForQuery(query);
for (const item of items) {
let best = 0;
let matchedText = '';
for (const get of getters) {
const value = get(item);
if (!value) continue;
const s = scoreMatch(query, value);
if (s > best) {
best = s;
matchedText = value;
}
if (best >= 95) {
break;
}
}
if (best >= threshold) results.push({ item, score: best, matchedText });
}
results.sort((a, b) => b.score - a.score);
return results;
}
export function normalizeForSearch(text: string): string {
return normalizeText(text);
}
// Convert ids like "addPassword", "add-password", "add_password" to words for matching
export function idToWords(id: string): string {
const spaced = id
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
.replace(/[._-]+/g, ' ');
return normalizeText(spaced);
}

View File

@ -0,0 +1,99 @@
import { ToolRegistryEntry } from "../data/toolsTaxonomy";
import { scoreMatch, minScoreForQuery, normalizeForSearch } from "./fuzzySearch";
export interface RankedToolItem {
item: [string, ToolRegistryEntry];
matchedText?: string;
}
export function filterToolRegistryByQuery(
toolRegistry: Record<string, ToolRegistryEntry>,
query: string
): RankedToolItem[] {
const entries = Object.entries(toolRegistry);
if (!query.trim()) {
return entries.map(([id, tool]) => ({ item: [id, tool] as [string, ToolRegistryEntry] }));
}
const nq = normalizeForSearch(query);
const threshold = minScoreForQuery(query);
const exactName: Array<{ id: string; tool: ToolRegistryEntry; pos: number }> = [];
const exactSyn: Array<{ id: string; tool: ToolRegistryEntry; text: string; pos: number }> = [];
const fuzzyName: Array<{ id: string; tool: ToolRegistryEntry; score: number; text: string }> = [];
const fuzzySyn: Array<{ id: string; tool: ToolRegistryEntry; score: number; text: string }> = [];
for (const [id, tool] of entries) {
const nameNorm = normalizeForSearch(tool.name || '');
const pos = nameNorm.indexOf(nq);
if (pos !== -1) {
exactName.push({ id, tool, pos });
continue;
}
const syns = Array.isArray(tool.synonyms) ? tool.synonyms : [];
let matchedExactSyn: { text: string; pos: number } | null = null;
for (const s of syns) {
const sn = normalizeForSearch(s);
const sp = sn.indexOf(nq);
if (sp !== -1) {
matchedExactSyn = { text: s, pos: sp };
break;
}
}
if (matchedExactSyn) {
exactSyn.push({ id, tool, text: matchedExactSyn.text, pos: matchedExactSyn.pos });
continue;
}
// Fuzzy name
const nameScore = scoreMatch(query, tool.name || '');
if (nameScore >= threshold) {
fuzzyName.push({ id, tool, score: nameScore, text: tool.name || '' });
}
// Fuzzy synonyms (we'll consider these only if fuzzy name results are weak)
let bestSynScore = 0;
let bestSynText = '';
for (const s of syns) {
const synScore = scoreMatch(query, s);
if (synScore > bestSynScore) {
bestSynScore = synScore;
bestSynText = s;
}
if (bestSynScore >= 95) break;
}
if (bestSynScore >= threshold) {
fuzzySyn.push({ id, tool, score: bestSynScore, text: bestSynText });
}
}
// Sort within buckets
exactName.sort((a, b) => a.pos - b.pos || (a.tool.name || '').length - (b.tool.name || '').length);
exactSyn.sort((a, b) => a.pos - b.pos || a.text.length - b.text.length);
fuzzyName.sort((a, b) => b.score - a.score);
fuzzySyn.sort((a, b) => b.score - a.score);
// Concatenate buckets with de-duplication by tool id
const seen = new Set<string>();
const ordered: RankedToolItem[] = [];
const push = (id: string, tool: ToolRegistryEntry, matchedText?: string) => {
if (seen.has(id)) return;
seen.add(id);
ordered.push({ item: [id, tool], matchedText });
};
for (const { id, tool } of exactName) push(id, tool, tool.name);
for (const { id, tool, text } of exactSyn) push(id, tool, text);
for (const { id, tool, text } of fuzzyName) push(id, tool, text);
for (const { id, tool, text } of fuzzySyn) push(id, tool, text);
if (ordered.length > 0) return ordered;
// Fallback: return everything unchanged
return entries.map(([id, tool]) => ({ item: [id, tool] as [string, ToolRegistryEntry] }));
}

View File

@ -0,0 +1,24 @@
import { TFunction } from 'i18next';
// Helper function to get synonyms for a tool (only from translations)
export const getSynonyms = (t: TFunction, toolId: string): string[] => {
try {
const tagsKey = `${toolId}.tags`;
const tags = t(tagsKey) as unknown as string;
// If the translation key doesn't exist or returns the key itself, return empty array
if (!tags || tags === tagsKey) {
return [];
}
// Split by comma and clean up the tags
return tags
.split(',')
.map((tag: string) => tag.trim())
.filter((tag: string) => tag.length > 0);
} catch (error) {
console.warn(`Failed to get translated synonyms for tool ${toolId}:`, error);
return [];
}};