Add test for missing translations (#4696)

# Description of Changes
Adds a test to scan the code for any static translation keys which are
not present in the GB translations file. The test won't catch every
missing translation present in our code, but it should greatly help us
keep the translations file up to date.
This commit is contained in:
James Brunton 2025-10-17 16:50:04 +01:00 committed by GitHub
parent 5354f08766
commit 3e6236d957
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 588 additions and 64 deletions

View File

@ -13,7 +13,19 @@
"dismiss": "Maybe later" "dismiss": "Maybe later"
}, },
"fullscreen": { "fullscreen": {
"showDetails": "Show Details" "showDetails": "Show Details",
"comingSoon": "Coming soon:",
"favorite": "Add to favourites",
"favorites": "Favourites",
"heading": "All tools (fullscreen view)",
"noResults": "Try adjusting your search or toggle descriptions to find what you need.",
"recommended": "Recommended",
"unfavorite": "Remove from favourites"
},
"placeholder": "Choose a tool to get started",
"toggle": {
"fullscreen": "Switch to fullscreen mode",
"sidebar": "Switch to sidebar mode"
} }
}, },
"unsavedChanges": "You have unsaved changes to your PDF.", "unsavedChanges": "You have unsaved changes to your PDF.",
@ -56,7 +68,7 @@
"preview": "Position Selection", "preview": "Position Selection",
"previewDisclaimer": "Preview is approximate. Final output may vary due to PDF font metrics." "previewDisclaimer": "Preview is approximate. Final output may vary due to PDF font metrics."
}, },
"pageSelectionPrompt": "Specify which pages to add numbers to. Examples: \"1,3,5\" for specific pages, \"1-5\" for ranges, \"2n\" for even pages, or leave blank for all pages.", "pageSelectionPrompt": "Custom Page Selection (Enter a comma-separated list of page numbers 1,5,6 or Functions like 2n+1)",
"startingNumberTooltip": "The first number to display. Subsequent pages will increment from this number.", "startingNumberTooltip": "The first number to display. Subsequent pages will increment from this number.",
"marginTooltip": "Distance between the page number and the edge of the page.", "marginTooltip": "Distance between the page number and the edge of the page.",
"fontSizeTooltip": "Size of the page number text in points. Larger numbers create bigger text.", "fontSizeTooltip": "Size of the page number text in points. Larger numbers create bigger text.",
@ -72,7 +84,6 @@
"uploadLimitExceededPlural": "are too large. Maximum allowed size is", "uploadLimitExceededPlural": "are too large. Maximum allowed size is",
"processTimeWarning": "Warning: This process can take up to a minute depending on file-size", "processTimeWarning": "Warning: This process can take up to a minute depending on file-size",
"pageOrderPrompt": "Custom Page Order (Enter a comma-separated list of page numbers or Functions like 2n+1) :", "pageOrderPrompt": "Custom Page Order (Enter a comma-separated list of page numbers or Functions like 2n+1) :",
"pageSelectionPrompt": "Custom Page Selection (Enter a comma-separated list of page numbers 1,5,6 or Functions like 2n+1) :",
"goToPage": "Go", "goToPage": "Go",
"true": "True", "true": "True",
"false": "False", "false": "False",
@ -104,7 +115,9 @@
"uploadFiles": "Upload Files", "uploadFiles": "Upload Files",
"addFiles": "Add files", "addFiles": "Add files",
"selectFromWorkbench": "Select files from the workbench or ", "selectFromWorkbench": "Select files from the workbench or ",
"selectMultipleFromWorkbench": "Select at least {{count}} files from the workbench or " "selectMultipleFromWorkbench": "Select at least {{count}} files from the workbench or ",
"created": "Created",
"size": "File Size"
}, },
"noFavourites": "No favourites added", "noFavourites": "No favourites added",
"downloadComplete": "Download Complete", "downloadComplete": "Download Complete",
@ -289,7 +302,13 @@
"autoUnzipTooltip": "Automatically extract ZIP files returned from API operations. Disable to keep ZIP files intact. This does not affect automation workflows.", "autoUnzipTooltip": "Automatically extract ZIP files returned from API operations. Disable to keep ZIP files intact. This does not affect automation workflows.",
"autoUnzipFileLimit": "Auto-unzip file limit", "autoUnzipFileLimit": "Auto-unzip file limit",
"autoUnzipFileLimitDescription": "Maximum number of files to extract from ZIP", "autoUnzipFileLimitDescription": "Maximum number of files to extract from ZIP",
"autoUnzipFileLimitTooltip": "Only unzip if the ZIP contains this many files or fewer. Set higher to extract larger ZIPs." "autoUnzipFileLimitTooltip": "Only unzip if the ZIP contains this many files or fewer. Set higher to extract larger ZIPs.",
"defaultToolPickerMode": "Default tool picker mode",
"defaultToolPickerModeDescription": "Choose whether the tool picker opens in fullscreen or sidebar by default",
"mode": {
"fullscreen": "Fullscreen",
"sidebar": "Sidebar"
}
}, },
"hotkeys": { "hotkeys": {
"title": "Keyboard Shortcuts", "title": "Keyboard Shortcuts",
@ -306,7 +325,8 @@
"change": "Change shortcut", "change": "Change shortcut",
"reset": "Reset", "reset": "Reset",
"shortcut": "Shortcut", "shortcut": "Shortcut",
"noShortcut": "No shortcut set" "noShortcut": "No shortcut set",
"searchPlaceholder": "Search tools..."
} }
}, },
"changeCreds": { "changeCreds": {
@ -665,9 +685,9 @@
"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": {
"tags": "view,open,display", "tags": "view,open,display",
"title": "Read", "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": {
@ -724,6 +744,20 @@
"tags": "workflow,sequence,automation", "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."
},
"mobile": {
"brandAlt": "Stirling PDF logo",
"openFiles": "Open files",
"swipeHint": "Swipe left or right to switch views",
"tools": "Tools",
"toolsSlide": "Tool selection panel",
"viewSwitcher": "Switch workspace view",
"workbenchSlide": "Workspace panel",
"workspace": "Workspace"
},
"overlay-pdfs": {
"desc": "Overlay one PDF on top of another",
"title": "Overlay PDFs"
} }
}, },
"landing": { "landing": {
@ -909,13 +943,50 @@
"bullet1": "Bookmark Level: Which level to split on (1=top level)", "bullet1": "Bookmark Level: Which level to split on (1=top level)",
"bullet2": "Include Metadata: Preserve document properties", "bullet2": "Include Metadata: Preserve document properties",
"bullet3": "Allow Duplicates: Handle repeated bookmark names" "bullet3": "Allow Duplicates: Handle repeated bookmark names"
},
"byDocCount": {
"bullet1": "Enter the number of output files you want",
"bullet2": "Pages are distributed as evenly as possible",
"bullet3": "Useful when you need a specific number of files",
"text": "Create a specific number of output files by evenly distributing pages across them.",
"title": "Split by Document Count"
},
"byPageCount": {
"bullet1": "Enter the number of pages per output file",
"bullet2": "Last file may have fewer pages if not evenly divisible",
"bullet3": "Useful for batch processing workflows",
"text": "Create multiple PDFs with a specific number of pages each. Perfect for creating uniform document chunks.",
"title": "Split by Page Count"
},
"byPageDivider": {
"bullet1": "Print divider sheets from the download link",
"bullet2": "Insert divider sheets between your documents",
"bullet3": "Scan all documents together as one PDF",
"bullet4": "Upload - divider pages are automatically detected and removed",
"bullet5": "Enable Duplex Mode if scanning both sides of divider sheets",
"text": "Automatically split scanned documents using physical divider sheets with QR codes. Perfect for processing multiple documents scanned together.",
"title": "Split by Page Divider"
} }
} },
"methodSelection": {
"tooltip": {
"bullet1": "Click on a method card to select it",
"bullet2": "Hover over each card to see a quick description",
"bullet3": "The settings step will appear after you select a method",
"bullet4": "You can change methods at any time before processing",
"header": {
"text": "Choose how you want to split your PDF document. Each method is optimized for different use cases and document types.",
"title": "Split Method Selection"
},
"title": "Choose Your Split Method"
}
},
"selectMethod": "Select a split method"
}, },
"rotate": { "rotate": {
"title": "Rotate PDF", "title": "Rotate PDF",
"submit": "Apply Rotation", "submit": "Apply Rotation",
"selectRotation": "Select Rotation Angle (Clockwise)", "selectRotation": "Select Rotation Angle (Clockwise)",
"error": { "error": {
"failed": "An error occurred while rotating the PDF." "failed": "An error occurred while rotating the PDF."
}, },
@ -1003,7 +1074,8 @@
"imagesExt": "Images (JPG, PNG, etc.)", "imagesExt": "Images (JPG, PNG, etc.)",
"markdown": "Markdown", "markdown": "Markdown",
"textRtf": "Text/RTF", "textRtf": "Text/RTF",
"grayscale": "Greyscale" "grayscale": "Greyscale",
"errorConversion": "An error occurred while converting the file."
}, },
"imageToPdf": { "imageToPdf": {
"tags": "conversion,img,jpg,picture,photo" "tags": "conversion,img,jpg,picture,photo"
@ -1030,7 +1102,7 @@
"header": "PDF Page Organiser", "header": "PDF Page Organiser",
"submit": "Rearrange Pages", "submit": "Rearrange Pages",
"mode": { "mode": {
"_value": "Organization mode", "_value": "Organisation mode",
"1": "Custom Page Order", "1": "Custom Page Order",
"2": "Reverse Order", "2": "Reverse Order",
"3": "Duplex Sort", "3": "Duplex Sort",
@ -1041,7 +1113,20 @@
"8": "Remove Last", "8": "Remove Last",
"9": "Remove First and Last", "9": "Remove First and Last",
"10": "Odd-Even Merge", "10": "Odd-Even Merge",
"11": "Duplicate all pages" "11": "Duplicate all pages",
"desc": {
"BOOKLET_SORT": "Arrange pages for booklet printing (last, first, second, second last, …).",
"CUSTOM": "Use a custom sequence of page numbers or expressions to define a new order.",
"DUPLEX_SORT": "Interleave fronts then backs as if a duplex scanner scanned all fronts, then all backs (1, n, 2, n-1, …).",
"DUPLICATE": "Duplicate each page according to the custom order count (e.g., 4 duplicates each page 4×).",
"ODD_EVEN_MERGE": "Merge two PDFs by alternating pages: odd from the first, even from the second.",
"ODD_EVEN_SPLIT": "Split the document into two outputs: all odd pages and all even pages.",
"REMOVE_FIRST": "Remove the first page from the document.",
"REMOVE_FIRST_AND_LAST": "Remove both the first and last pages from the document.",
"REMOVE_LAST": "Remove the last page from the document.",
"REVERSE_ORDER": "Flip the document so the last page becomes first and so on.",
"SIDE_STITCH_BOOKLET_SORT": "Arrange pages for sidestitch booklet printing (optimized for binding on the side)."
}
}, },
"desc": { "desc": {
"CUSTOM": "Use a custom sequence of page numbers or expressions to define a new order.", "CUSTOM": "Use a custom sequence of page numbers or expressions to define a new order.",
@ -1107,7 +1192,9 @@
"opacity": "Opacity (%)", "opacity": "Opacity (%)",
"spacing": { "spacing": {
"horizontal": "Horizontal Spacing", "horizontal": "Horizontal Spacing",
"vertical": "Vertical Spacing" "vertical": "Vertical Spacing",
"height": "Height Spacing",
"width": "Width Spacing"
}, },
"convertToImage": "Flatten PDF pages to images" "convertToImage": "Flatten PDF pages to images"
}, },
@ -1250,6 +1337,10 @@
"bullet4": "Best for sensitive or copyrighted content" "bullet4": "Best for sensitive or copyrighted content"
} }
} }
},
"type": {
"1": "Text",
"2": "Image"
} }
}, },
"permissions": { "permissions": {
@ -1363,6 +1454,38 @@
}, },
"examples": { "examples": {
"title": "Examples" "title": "Examples"
},
"complex": {
"bullet1": "<strong>1,3-5,8,2n</strong> → pages 1, 35, 8, plus evens",
"bullet2": "<strong>10-,2n-1</strong> → from page 10 to end + odd pages",
"description": "Mix different types.",
"title": "Complex Combinations"
},
"description": "Choose which pages to use for the operation. Supports single pages, ranges, formulas, and the all keyword.",
"individual": {
"bullet1": "<strong>1,3,5</strong> → selects pages 1, 3, 5",
"bullet2": "<strong>2,7,12</strong> → selects pages 2, 7, 12",
"description": "Enter numbers separated by commas.",
"title": "Individual Pages"
},
"mathematical": {
"bullet1": "<strong>2n</strong> → all even pages (2, 4, 6…)",
"bullet2": "<strong>2n-1</strong> → all odd pages (1, 3, 5…)",
"bullet3": "<strong>3n</strong> → every 3rd page (3, 6, 9…)",
"bullet4": "<strong>4n-1</strong> → pages 3, 7, 11, 15…",
"description": "Use n in formulas for patterns.",
"title": "Mathematical Functions"
},
"ranges": {
"bullet1": "<strong>3-6</strong> → selects pages 36",
"bullet2": "<strong>10-15</strong> → selects pages 1015",
"bullet3": "<strong>5-</strong> → selects pages 5 to end",
"description": "Use - for consecutive pages.",
"title": "Page Ranges"
},
"special": {
"bullet1": "<strong>all</strong> → selects all pages",
"title": "Special Keywords"
} }
} }
}, },
@ -1672,6 +1795,9 @@
"text": "Post-processes the final PDF by removing OCR artefacts and optimising the text layer for better readability and smaller file size." "text": "Post-processes the final PDF by removing OCR artefacts and optimising the text layer for better readability and smaller file size."
} }
} }
},
"error": {
"failed": "OCR operation failed"
} }
}, },
"extractImages": { "extractImages": {
@ -1792,8 +1918,14 @@
"title": "Sign", "title": "Sign",
"header": "Sign PDFs", "header": "Sign PDFs",
"upload": "Upload Image", "upload": "Upload Image",
"draw": "Draw Signature", "draw": {
"text": "Text Input", "title": "Draw your signature",
"clear": "Clear"
},
"text": {
"name": "Signer Name",
"placeholder": "Enter your full name"
},
"clear": "Clear", "clear": "Clear",
"add": "Add", "add": "Add",
"saved": "Saved Signatures", "saved": "Saved Signatures",
@ -1822,19 +1954,11 @@
"image": "Image", "image": "Image",
"text": "Text" "text": "Text"
}, },
"draw": {
"title": "Draw your signature",
"clear": "Clear"
},
"image": { "image": {
"label": "Upload signature image", "label": "Upload signature image",
"placeholder": "Select image file", "placeholder": "Select image file",
"hint": "Upload a PNG or JPG image of your signature" "hint": "Upload a PNG or JPG image of your signature"
}, },
"text": {
"name": "Signer Name",
"placeholder": "Enter your full name"
},
"instructions": { "instructions": {
"title": "How to add signature", "title": "How to add signature",
"canvas": "After drawing your signature in the canvas, close the modal then click anywhere on the PDF to place it.", "canvas": "After drawing your signature in the canvas, close the modal then click anywhere on the PDF to place it.",
@ -1961,7 +2085,13 @@
"bullet3": "Can be disabled to reduce output file size" "bullet3": "Can be disabled to reduce output file size"
} }
}, },
"submit": "Remove blank pages" "submit": "Remove blank pages",
"error": {
"failed": "Failed to remove blank pages"
},
"results": {
"title": "Removed Blank Pages"
}
}, },
"removeAnnotations": { "removeAnnotations": {
"tags": "comments,highlight,notes,markup,remove", "tags": "comments,highlight,notes,markup,remove",
@ -2063,7 +2193,12 @@
"bullet3": "Choose which page to place the signature", "bullet3": "Choose which page to place the signature",
"bullet4": "Optional logo can be included" "bullet4": "Optional logo can be included"
} }
} },
"invisible": "Invisible",
"options": {
"title": "Signature Details"
},
"visible": "Visible"
}, },
"sign": { "sign": {
"submit": "Sign PDF", "submit": "Sign PDF",
@ -2124,7 +2259,22 @@
"text": "Convert your file to a Java keystore (.jks) with keytool, then pick JKS." "text": "Convert your file to a Java keystore (.jks) with keytool, then pick JKS."
} }
} }
} },
"chooseCertificate": "Choose Certificate File",
"chooseJksFile": "Choose JKS File",
"chooseP12File": "Choose PKCS12 File",
"choosePfxFile": "Choose PFX File",
"choosePrivateKey": "Choose Private Key File",
"location": "Location",
"logoTitle": "Logo",
"name": "Name",
"noLogo": "No Logo",
"pageNumber": "Page Number",
"password": "Certificate Password",
"passwordOptional": "Leave empty if no password",
"reason": "Reason",
"serverCertMessage": "Using server certificate - no files or password required",
"showLogo": "Show Logo"
}, },
"removeCertSign": { "removeCertSign": {
"tags": "authenticate,PEM,P12,official,decrypt", "tags": "authenticate,PEM,P12,official,decrypt",
@ -2150,7 +2300,17 @@
"header": "Multi Page Layout", "header": "Multi Page Layout",
"pagesPerSheet": "Pages per sheet:", "pagesPerSheet": "Pages per sheet:",
"addBorder": "Add Borders", "addBorder": "Add Borders",
"submit": "Submit" "submit": "Submit",
"desc": {
"2": "Place 2 pages side-by-side on a single sheet.",
"3": "Place 3 pages on a single sheet in a single row.",
"4": "Place 4 pages on a single sheet (2 × 2 grid).",
"9": "Place 9 pages on a single sheet (3 × 3 grid).",
"16": "Place 16 pages on a single sheet (4 × 4 grid)."
},
"error": {
"failed": "An error occurred while creating the multi-page layout."
}
}, },
"bookletImposition": { "bookletImposition": {
"tags": "booklet,imposition,printing,binding,folding,signature", "tags": "booklet,imposition,printing,binding,folding,signature",
@ -2336,10 +2496,22 @@
"reset": "Reset to full PDF", "reset": "Reset to full PDF",
"coordinates": { "coordinates": {
"title": "Position and Size", "title": "Position and Size",
"x": "X Position", "x": {
"y": "Y Position", "label": "X Position",
"width": "Width", "desc": "Left edge (points)"
"height": "Height" },
"y": {
"label": "Y Position",
"desc": "Bottom edge (points)"
},
"width": {
"label": "Width",
"desc": "Crop width (points)"
},
"height": {
"label": "Height",
"desc": "Crop height (points)"
}
}, },
"error": { "error": {
"invalidArea": "Crop area extends beyond PDF boundaries", "invalidArea": "Crop area extends beyond PDF boundaries",
@ -2357,6 +2529,10 @@
}, },
"results": { "results": {
"title": "Crop Results" "title": "Crop Results"
},
"automation": {
"info": "Enter crop coordinates in PDF points. Origin (0,0) is at bottom-left. These values will be applied to all PDFs processed in this automation.",
"reference": "Reference: A4 page is 595.28 × 841.89 points (210mm × 297mm). 1 inch = 72 points."
} }
}, },
"autoSplitPDF": { "autoSplitPDF": {
@ -2586,7 +2762,8 @@
"counts": { "counts": {
"label": "Overlay Counts (for Fixed Repeat Mode)", "label": "Overlay Counts (for Fixed Repeat Mode)",
"placeholder": "Enter comma-separated counts (e.g., 2,3,1)", "placeholder": "Enter comma-separated counts (e.g., 2,3,1)",
"item": "Count for file" "item": "Count for file",
"noFiles": "Add overlay files to configure counts"
}, },
"position": { "position": {
"label": "Select Overlay Position", "label": "Select Overlay Position",
@ -2627,6 +2804,9 @@
"title": "Counts (Fixed Repeat only)", "title": "Counts (Fixed Repeat only)",
"text": "Provide a positive number for each overlay file showing how many pages to take before moving to the next. Required when mode is Fixed Repeat." "text": "Provide a positive number for each overlay file showing how many pages to take before moving to the next. Required when mode is Fixed Repeat."
} }
},
"error": {
"failed": "An error occurred while overlaying PDFs."
} }
}, },
"split-by-sections": { "split-by-sections": {
@ -2662,7 +2842,18 @@
"customMargin": "Custom Margin", "customMargin": "Custom Margin",
"customColor": "Custom Text Colour", "customColor": "Custom Text Colour",
"submit": "Submit", "submit": "Submit",
"noStampSelected": "No stamp selected. Return to Step 1." "noStampSelected": "No stamp selected. Return to Step 1.",
"customPosition": "Drag the stamp to the desired location in the preview window.",
"error": {
"failed": "An error occurred while adding stamp to the PDF."
},
"imageSize": "Image Size",
"margin": "Margin",
"positionAndFormatting": "Position & Formatting",
"quickPosition": "Select a position on the page to place the stamp.",
"results": {
"title": "Stamp Results"
}
}, },
"removeImagePdf": { "removeImagePdf": {
"tags": "Remove Image,Page operations,Back end,server side" "tags": "Remove Image,Page operations,Back end,server side"
@ -2680,7 +2871,8 @@
"status": { "status": {
"_value": "Status", "_value": "Status",
"valid": "Valid", "valid": "Valid",
"invalid": "Invalid" "invalid": "Invalid",
"complete": "Validation complete"
}, },
"signer": "Signer", "signer": "Signer",
"date": "Date", "date": "Date",
@ -2707,16 +2899,71 @@
"version": "Version", "version": "Version",
"keyUsage": "Key Usage", "keyUsage": "Key Usage",
"selfSigned": "Self-Signed", "selfSigned": "Self-Signed",
"bits": "bits" "bits": "bits",
"details": "Certificate Details"
}, },
"signature": { "signature": {
"info": "Signature Information", "info": "Signature Information",
"_value": "Signature", "_value": "Signature",
"mathValid": "Signature is mathematically valid BUT:" "mathValid": "Signature is mathematically valid BUT:"
}, },
"selectCustomCert": "Custom Certificate File X.509 (Optional)" "selectCustomCert": "Custom Certificate File X.509 (Optional)",
"downloadCsv": "Download CSV",
"downloadJson": "Download JSON",
"downloadPdf": "Download PDF Report",
"downloadType": {
"csv": "CSV",
"json": "JSON",
"pdf": "PDF"
},
"error": {
"allFailed": "Unable to validate the selected files.",
"partial": "Some files could not be validated.",
"reportGeneration": "Could not generate the PDF report. JSON and CSV are available.",
"unexpected": "Unexpected error during validation."
},
"finalizing": "Preparing downloads...",
"issue": {
"certExpired": "Certificate expired",
"certRevocationUnknown": "Certificate revocation status unknown",
"certRevoked": "Certificate revoked",
"chainInvalid": "Certificate chain invalid",
"signatureInvalid": "Signature cryptographic check failed",
"trustInvalid": "Certificate not trusted"
},
"noResults": "Run the validation to generate a report.",
"noSignaturesShort": "No signatures",
"processing": "Validating signatures...",
"report": {
"continued": "Continued",
"downloads": "Downloads",
"entryLabel": "Signature Summary",
"fields": {
"created": "Created",
"fileSize": "File Size",
"signatureCount": "Total Signatures",
"signatureDate": "Signature Date"
},
"filesEvaluated": "{{count}} files evaluated",
"footer": "Validated via Stirling PDF",
"generatedAt": "Generated",
"noPdf": "PDF report will be available after a successful validation.",
"page": "Page",
"shortTitle": "Signature Summary",
"signatureCountLabel": "{{count}} signatures",
"signaturesFound": "{{count}} signatures detected",
"signaturesValid": "{{count}} fully valid",
"title": "Signature Validation Report"
},
"settings": {
"certHint": "Upload a trusted X.509 certificate to validate against a custom trust source.",
"title": "Validation Settings"
},
"signatureDate": "Signature Date",
"totalSignatures": "Total Signatures"
}, },
"replaceColor": { "replaceColor": {
"tags": "Replace Colour,Page operations,Back end,server side",
"labels": { "labels": {
"settings": "Settings", "settings": "Settings",
"colourOperation": "Colour operation" "colourOperation": "Colour operation"
@ -2757,9 +3004,6 @@
"failed": "An error occurred while processing the colour replacement." "failed": "An error occurred while processing the colour replacement."
} }
}, },
"replaceColor": {
"tags": "Replace Colour,Page operations,Back end,server side"
},
"login": { "login": {
"title": "Sign in", "title": "Sign in",
"header": "Sign in", "header": "Sign in",
@ -2875,7 +3119,19 @@
"contrast": "Contrast:", "contrast": "Contrast:",
"brightness": "Brightness:", "brightness": "Brightness:",
"saturation": "Saturation:", "saturation": "Saturation:",
"download": "Download" "download": "Download",
"adjustColors": "Adjust Colors",
"blue": "Blue",
"confirm": "Confirm",
"error": {
"failed": "Failed to adjust colors/contrast"
},
"green": "Green",
"noPreview": "Select a PDF to preview",
"red": "Red",
"results": {
"title": "Adjusted PDF"
}
}, },
"compress": { "compress": {
"title": "Compress", "title": "Compress",
@ -3022,7 +3278,13 @@
"title": "Remove image", "title": "Remove image",
"header": "Remove image", "header": "Remove image",
"removeImage": "Remove image", "removeImage": "Remove image",
"submit": "Remove image" "submit": "Remove image",
"error": {
"failed": "Failed to remove images from the PDF."
},
"results": {
"title": "Remove Images Results"
}
}, },
"splitByChapters": { "splitByChapters": {
"title": "Split PDF by Chapters", "title": "Split PDF by Chapters",
@ -3167,7 +3429,9 @@
}, },
"search": { "search": {
"title": "Search PDF", "title": "Search PDF",
"placeholder": "Enter search term..." "placeholder": "Enter search term...",
"noResults": "No results found",
"searching": "Searching..."
}, },
"guestBanner": { "guestBanner": {
"title": "You're using Stirling PDF as a guest!", "title": "You're using Stirling PDF as a guest!",
@ -3289,7 +3553,17 @@
"selectedCount": "{{count}} selected", "selectedCount": "{{count}} selected",
"download": "Download", "download": "Download",
"delete": "Delete", "delete": "Delete",
"unsupported": "Unsupported" "unsupported": "Unsupported",
"addToUpload": "Add to Upload",
"deleteAll": "Delete All",
"loadingFiles": "Loading files...",
"noFiles": "No files available",
"noFilesFound": "No files found matching your search",
"openInPageEditor": "Open in Page Editor",
"showAll": "Show All",
"sortByDate": "Sort by Date",
"sortByName": "Sort by Name",
"sortBySize": "Sort by Size"
}, },
"storage": { "storage": {
"temporaryNotice": "Files are stored temporarily in your browser and may be cleared automatically", "temporaryNotice": "Files are stored temporarily in your browser and may be cleared automatically",
@ -3544,16 +3818,6 @@
"processImagesDesc": "Converts multiple image files into a single PDF document, then applies OCR technology to extract searchable text from the images." "processImagesDesc": "Converts multiple image files into a single PDF document, then applies OCR technology to extract searchable text from the images."
} }
}, },
"viewer": {
"firstPage": "First Page",
"lastPage": "Last Page",
"previousPage": "Previous Page",
"nextPage": "Next Page",
"zoomIn": "Zoom In",
"zoomOut": "Zoom Out",
"singlePageView": "Single Page View",
"dualPageView": "Dual Page View"
},
"common": { "common": {
"copy": "Copy", "copy": "Copy",
"copied": "Copied!", "copied": "Copied!",
@ -3562,7 +3826,8 @@
"remaining": "remaining", "remaining": "remaining",
"used": "used", "used": "used",
"available": "available", "available": "available",
"cancel": "Cancel" "cancel": "Cancel",
"preview": "Preview"
}, },
"config": { "config": {
"account": { "account": {
@ -3620,8 +3885,89 @@
"submit": "Add Attachments", "submit": "Add Attachments",
"results": { "results": {
"title": "Attachment Results" "title": "Attachment Results"
},
"error": {
"failed": "Add attachments operation failed"
} }
}, },
"termsAndConditions": "Terms & Conditions", "termsAndConditions": "Terms & Conditions",
"logOut": "Log out" "logOut": "Log out",
"addAttachments": {
"error": {
"failed": "An error occurred while adding attachments to the PDF."
}
},
"autoRename": {
"description": "This tool will automatically rename PDF files based on their content. It analyzes the document to find the most suitable title from the text."
},
"customPosition": "Custom Position",
"details": "Details",
"downloadUnavailable": "Download unavailable for this item",
"invalidUndoData": "Cannot undo: invalid operation data",
"margin": {
"large": "Large",
"medium": "Medium",
"small": "Small",
"xLarge": "Extra Large"
},
"noFilesToUndo": "Cannot undo: no files were processed in the last operation",
"noOperationToUndo": "No operation to undo",
"noValidFiles": "No valid files to process",
"operationCancelled": "Operation cancelled",
"pageEdit": {
"deselectAll": "Select None",
"selectAll": "Select All"
},
"quickPosition": "Quick Position",
"reorganizePages": {
"error": {
"failed": "Failed to reorganize pages"
},
"results": {
"title": "Pages Reorganized"
},
"settings": {
"title": "Settings"
},
"submit": "Reorganize Pages"
},
"replace-color": {
"options": {
"fill": "Fill colour",
"gradient": "Gradient"
},
"previewOverlayOpacity": "Preview overlay opacity",
"previewOverlayTransparency": "Preview overlay transparency",
"previewOverlayVisibility": "Show preview overlay",
"selectText": {
"1": "Replace or invert colour options",
"2": "Default (preset high contrast colours)",
"3": "Custom (choose your own colours)",
"4": "Full invert (invert all colours)",
"5": "High contrast color options",
"6": "White text on black background",
"7": "Black text on white background",
"8": "Yellow text on black background",
"9": "Green text on black background",
"10": "Choose text Color",
"11": "Choose background Color",
"12": "Choose start colour",
"13": "Choose end colour"
},
"submit": "Replace",
"title": "Replace-Invert-Color"
},
"size": "Size",
"submit": "Submit",
"success": "Success",
"tools": {
"noSearchResults": "No tools found",
"noTools": "No tools available"
},
"undoDataMismatch": "Cannot undo: operation data is corrupted",
"undoFailed": "Failed to undo operation",
"undoQuotaError": "Cannot undo: insufficient storage space",
"undoStorageError": "Undo completed but some files could not be saved to storage",
"undoSuccess": "Operation undone successfully",
"unsupported": "Unsupported"
} }

View File

@ -43,7 +43,7 @@ const AddPageNumbersPositionSettings = ({
<Stack gap="md"> <Stack gap="md">
<Text size="sm" fw={500} mb="xs">{t('addPageNumbers.pagesAndStarting', 'Pages & Starting Number')}</Text> <Text size="sm" fw={500} mb="xs">{t('addPageNumbers.pagesAndStarting', 'Pages & Starting Number')}</Text>
<Tooltip content={t('pageSelectionPrompt', 'Specify which pages to add numbers to. Examples: "1,3,5" for specific pages, "1-5" for ranges, "2n" for even pages, or leave blank for all pages.')}> <Tooltip content={t('pageSelectionPrompt', 'Custom Page Selection (Enter a comma-separated list of page numbers 1,5,6 or Functions like 2n+1)')}>
<TextInput <TextInput
label={t('addPageNumbers.selectText.5', 'Pages to Number')} label={t('addPageNumbers.selectText.5', 'Pages to Number')}
value={parameters.pagesToNumber || ''} value={parameters.pagesToNumber || ''}

View File

@ -35,7 +35,7 @@ const CropCoordinateInputs = ({
<Group grow> <Group grow>
<NumberInput <NumberInput
label={t("crop.coordinates.x", "X Position")} label={t("crop.coordinates.x.label", "X Position")}
description={showAutomationInfo ? t("crop.coordinates.x.desc", "Left edge (points)") : undefined} description={showAutomationInfo ? t("crop.coordinates.x.desc", "Left edge (points)") : undefined}
value={Math.round(cropArea.x * 10) / 10} value={Math.round(cropArea.x * 10) / 10}
onChange={(value) => onCoordinateChange('x', value)} onChange={(value) => onCoordinateChange('x', value)}
@ -47,7 +47,7 @@ const CropCoordinateInputs = ({
size={showAutomationInfo ? "sm" : "xs"} size={showAutomationInfo ? "sm" : "xs"}
/> />
<NumberInput <NumberInput
label={t("crop.coordinates.y", "Y Position")} label={t("crop.coordinates.y.label", "Y Position")}
description={showAutomationInfo ? t("crop.coordinates.y.desc", "Bottom edge (points)") : undefined} description={showAutomationInfo ? t("crop.coordinates.y.desc", "Bottom edge (points)") : undefined}
value={Math.round(cropArea.y * 10) / 10} value={Math.round(cropArea.y * 10) / 10}
onChange={(value) => onCoordinateChange('y', value)} onChange={(value) => onCoordinateChange('y', value)}
@ -62,7 +62,7 @@ const CropCoordinateInputs = ({
<Group grow> <Group grow>
<NumberInput <NumberInput
label={t("crop.coordinates.width", "Width")} label={t("crop.coordinates.width.label", "Width")}
description={showAutomationInfo ? t("crop.coordinates.width.desc", "Crop width (points)") : undefined} description={showAutomationInfo ? t("crop.coordinates.width.desc", "Crop width (points)") : undefined}
value={Math.round(cropArea.width * 10) / 10} value={Math.round(cropArea.width * 10) / 10}
onChange={(value) => onCoordinateChange('width', value)} onChange={(value) => onCoordinateChange('width', value)}
@ -74,7 +74,7 @@ const CropCoordinateInputs = ({
size={showAutomationInfo ? "sm" : "xs"} size={showAutomationInfo ? "sm" : "xs"}
/> />
<NumberInput <NumberInput
label={t("crop.coordinates.height", "Height")} label={t("crop.coordinates.height.label", "Height")}
description={showAutomationInfo ? t("crop.coordinates.height.desc", "Crop height (points)") : undefined} description={showAutomationInfo ? t("crop.coordinates.height.desc", "Crop height (points)") : undefined}
value={Math.round(cropArea.height * 10) / 10} value={Math.round(cropArea.height * 10) / 10}
onChange={(value) => onCoordinateChange('height', value)} onChange={(value) => onCoordinateChange('height', value)}

View File

@ -33,6 +33,6 @@ export const useReplaceColorOperation = () => {
return useToolOperation<ReplaceColorParameters>({ return useToolOperation<ReplaceColorParameters>({
...replaceColorOperationConfig, ...replaceColorOperationConfig,
getErrorMessage: createStandardErrorHandler(t('replaceColor.error.failed', 'An error occurred while processing the color replacement.')) getErrorMessage: createStandardErrorHandler(t('replaceColor.error.failed', 'An error occurred while processing the colour replacement.'))
}); });
}; };

View File

@ -0,0 +1,178 @@
import fs from 'fs';
import path from 'path';
import ts from 'typescript';
import { describe, expect, test } from 'vitest';
const REPO_ROOT = path.join(__dirname, '../../../')
const SRC_ROOT = path.join(__dirname, '..');
const EN_GB_FILE = path.join(__dirname, '../../public/locales/en-GB/translation.json');
const IGNORED_DIRS = new Set([
'tests',
'__mocks__',
]);
const IGNORED_FILE_PATTERNS = [
/\.d\.ts$/,
/\.test\./,
/\.spec\./,
/\.stories\./,
];
const IGNORED_KEYS = new Set<string>([
// If the script has found a false-positive that shouldn't be in the translations, include it here
]);
type FoundKey = {
key: string;
file: string;
line: number;
column: number;
};
const flattenKeys = (node: unknown, prefix = '', acc = new Set<string>()): Set<string> => {
if (!node || typeof node !== 'object' || Array.isArray(node)) {
if (prefix) {
acc.add(prefix);
}
return acc;
}
for (const [childKey, value] of Object.entries(node as Record<string, unknown>)) {
const next = prefix ? `${prefix}.${childKey}` : childKey;
flattenKeys(value, next, acc);
}
return acc;
};
const listSourceFiles = (): string[] => {
const files = ts.sys.readDirectory(SRC_ROOT, ['.ts', '.tsx', '.js', '.jsx'], undefined, [
'**/*',
]);
return files
.filter((file) => !file.split(path.sep).some((segment) => IGNORED_DIRS.has(segment)))
.filter((file) => !IGNORED_FILE_PATTERNS.some((re) => re.test(file)));
};
const getScriptKind = (file: string): ts.ScriptKind => {
if (file.endsWith('.tsx')) {
return ts.ScriptKind.TSX;
}
if (file.endsWith('.ts')) {
return ts.ScriptKind.TS;
}
if (file.endsWith('.jsx')) {
return ts.ScriptKind.JSX;
}
return ts.ScriptKind.JS;
};
/**
* Find all of the static first keys for translation functions that we can.
* Ignores dynamic strings because we can't know what the actual translation key will be.
*/
const extractKeys = (file: string): FoundKey[] => {
const code = fs.readFileSync(file, 'utf8');
const sourceFile = ts.createSourceFile(
file,
code,
ts.ScriptTarget.Latest,
true,
getScriptKind(file),
);
const found: FoundKey[] = [];
const record = (node: ts.Node, key: string) => {
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
found.push({ key, file, line: line + 1, column: character + 1 });
};
const visit = (node: ts.Node) => {
if (ts.isCallExpression(node)) {
const callee = node.expression;
const arg = node.arguments.at(0);
const isT =
(ts.isIdentifier(callee) && callee.text === 't') ||
(ts.isPropertyAccessExpression(callee) && callee.name.text === 't');
if (isT && arg && (ts.isStringLiteral(arg) || ts.isNoSubstitutionTemplateLiteral(arg))) {
record(arg, arg.text);
}
}
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
for (const attr of node.attributes.properties) {
if (
!ts.isJsxAttribute(attr) ||
attr.name.getText(sourceFile) !== 'i18nKey' ||
!attr.initializer
) {
continue;
}
const init = attr.initializer;
if (ts.isStringLiteral(init)) {
record(init, init.text);
continue;
}
if (
ts.isJsxExpression(init) &&
init.expression &&
ts.isStringLiteral(init.expression)
) {
record(init.expression, init.expression.text);
}
}
}
ts.forEachChild(node, visit);
};
ts.forEachChild(sourceFile, visit);
return found;
};
describe('Missing translation coverage', () => {
test('fails if any en-GB translation key used in source is missing', () => {
expect(fs.existsSync(EN_GB_FILE)).toBe(true);
const localeContent = fs.readFileSync(EN_GB_FILE, 'utf8');
const enGb = JSON.parse(localeContent);
const availableKeys = flattenKeys(enGb);
const usedKeys = listSourceFiles()
.flatMap(extractKeys)
.filter(({ key }) => !IGNORED_KEYS.has(key));
expect(usedKeys.length).toBeGreaterThan(100); // Sanity check
const missingKeys = usedKeys.filter(({ key }) => !availableKeys.has(key));
const annotations = missingKeys.map(({ key, file, line, column }) => {
const workspaceRelativeRaw = path.relative(REPO_ROOT, file);
const workspaceRelativeFile = workspaceRelativeRaw.replace(/\\/g, '/');
return {
key,
file: workspaceRelativeFile,
line,
column,
};
});
// Output errors in GitHub Annotations format so they appear tagged in the code in CI
for (const { key, file, line, column } of annotations) {
process.stderr.write(
`::error file=${file},line=${line},col=${column}::Missing en-GB translation for ${key}\n`,
);
}
expect(missingKeys).toEqual([]);
});
});