diff --git a/common/src/main/java/stirling/software/common/service/CustomPDFDocumentFactory.java b/common/src/main/java/stirling/software/common/service/CustomPDFDocumentFactory.java index 24d1f4255..25b3d9501 100644 --- a/common/src/main/java/stirling/software/common/service/CustomPDFDocumentFactory.java +++ b/common/src/main/java/stirling/software/common/service/CustomPDFDocumentFactory.java @@ -83,7 +83,7 @@ public class CustomPDFDocumentFactory { */ public PDDocument load(File file, boolean readOnly) throws IOException { if (file == null) { - throw new IllegalArgumentException("File cannot be null"); + throw ExceptionUtils.createNullArgumentException("File"); } long fileSize = file.length(); @@ -110,7 +110,7 @@ public class CustomPDFDocumentFactory { */ public PDDocument load(Path path, boolean readOnly) throws IOException { if (path == null) { - throw new IllegalArgumentException("File cannot be null"); + throw ExceptionUtils.createNullArgumentException("File"); } long fileSize = Files.size(path); @@ -131,7 +131,7 @@ public class CustomPDFDocumentFactory { /** Load a PDF from byte array with automatic optimization and read-only option. */ public PDDocument load(byte[] input, boolean readOnly) throws IOException { if (input == null) { - throw new IllegalArgumentException("Input bytes cannot be null"); + throw ExceptionUtils.createNullArgumentException("Input bytes"); } long dataSize = input.length; @@ -152,7 +152,7 @@ public class CustomPDFDocumentFactory { /** Load a PDF from InputStream with automatic optimization and read-only option. */ public PDDocument load(InputStream input, boolean readOnly) throws IOException { if (input == null) { - throw new IllegalArgumentException("InputStream cannot be null"); + throw ExceptionUtils.createNullArgumentException("InputStream"); } // Since we don't know the size upfront, buffer to a temp file @@ -175,7 +175,7 @@ public class CustomPDFDocumentFactory { public PDDocument load(InputStream input, String password, boolean readOnly) throws IOException { if (input == null) { - throw new IllegalArgumentException("InputStream cannot be null"); + throw ExceptionUtils.createNullArgumentException("InputStream"); } // Since we don't know the size upfront, buffer to a temp file diff --git a/common/src/main/java/stirling/software/common/util/ExceptionUtils.java b/common/src/main/java/stirling/software/common/util/ExceptionUtils.java index 5e5dabf86..331587b13 100644 --- a/common/src/main/java/stirling/software/common/util/ExceptionUtils.java +++ b/common/src/main/java/stirling/software/common/util/ExceptionUtils.java @@ -137,6 +137,23 @@ public class ExceptionUtils { return new TranslatableException(message, messageKey, args); } + /** Create common validation exceptions with translation support. */ + public static IllegalArgumentException createInvalidArgumentException(String argumentName) { + return createIllegalArgumentException( + "error.invalidArgument", "Invalid argument: {0}", argumentName); + } + + public static IllegalArgumentException createInvalidArgumentException( + String argumentName, String value) { + return createIllegalArgumentException( + "error.invalidFormat", "Invalid {0} format: {1}", argumentName, value); + } + + public static IllegalArgumentException createNullArgumentException(String argumentName) { + return createIllegalArgumentException( + "error.argumentRequired", "{0} must not be null", argumentName); + } + /** Create file validation exceptions. */ public static IllegalArgumentException createHtmlFileRequiredException() { return createIllegalArgumentException( diff --git a/common/src/main/java/stirling/software/common/util/PdfUtils.java b/common/src/main/java/stirling/software/common/util/PdfUtils.java index 3b1d901d7..5c5bda000 100644 --- a/common/src/main/java/stirling/software/common/util/PdfUtils.java +++ b/common/src/main/java/stirling/software/common/util/PdfUtils.java @@ -571,8 +571,7 @@ public class PdfUtils { case "less": return actualPageCount < pageCount; default: - throw new IllegalArgumentException( - "Invalid comparator. Only 'greater', 'equal', and 'less' are supported."); + throw ExceptionUtils.createInvalidArgumentException("comparator", comparator); } } diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java index 96322ad80..ce9dab8c5 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java @@ -25,6 +25,7 @@ import stirling.software.SPDF.model.api.filter.FileSizeRequest; import stirling.software.SPDF.model.api.filter.PageRotationRequest; import stirling.software.SPDF.model.api.filter.PageSizeRequest; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.PdfUtils; import stirling.software.common.util.WebResponseUtils; @@ -96,7 +97,7 @@ public class FilterController { valid = actualPageCount < pageCount; break; default: - throw new IllegalArgumentException("Invalid comparator: " + comparator); + throw ExceptionUtils.createInvalidArgumentException("comparator", comparator); } if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile); @@ -139,7 +140,7 @@ public class FilterController { valid = actualArea < standardArea; break; default: - throw new IllegalArgumentException("Invalid comparator: " + comparator); + throw ExceptionUtils.createInvalidArgumentException("comparator", comparator); } if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile); @@ -172,7 +173,7 @@ public class FilterController { valid = actualFileSize < fileSize; break; default: - throw new IllegalArgumentException("Invalid comparator: " + comparator); + throw ExceptionUtils.createInvalidArgumentException("comparator", comparator); } if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile); @@ -208,7 +209,7 @@ public class FilterController { valid = actualRotation < rotation; break; default: - throw new IllegalArgumentException("Invalid comparator: " + comparator); + throw ExceptionUtils.createInvalidArgumentException("comparator", comparator); } if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile); diff --git a/stirling-pdf/src/main/resources/messages_en_GB.properties b/stirling-pdf/src/main/resources/messages_en_GB.properties index 2b1039e30..d95103f75 100644 --- a/stirling-pdf/src/main/resources/messages_en_GB.properties +++ b/stirling-pdf/src/main/resources/messages_en_GB.properties @@ -225,6 +225,7 @@ error.pathTraversalDetected=Path traversal detected for security reasons. # Validation messages error.invalidArgument=Invalid argument: {0} +error.argumentRequired={0} must not be null error.operationFailed=Operation failed: {0} error.angleNotMultipleOf90=Angle must be a multiple of 90 error.pdfBookmarksNotFound=No PDF bookmarks/outline found in document diff --git a/stirling-pdf/src/main/resources/static/js/downloader.js b/stirling-pdf/src/main/resources/static/js/downloader.js index 5c13f9e23..a393ee8b3 100644 --- a/stirling-pdf/src/main/resources/static/js/downloader.js +++ b/stirling-pdf/src/main/resources/static/js/downloader.js @@ -348,19 +348,13 @@ // Handle structured error response with translation support let displayMessage = json.message; - // If translation info is available, use it to translate the message - if (json.translationKey && window.stirlingPDF && window.stirlingPDF.translations) { - const translatedTemplate = window.stirlingPDF.translations[json.translationKey]; - if (translatedTemplate) { - displayMessage = translatedTemplate; - - // Replace placeholders with args if available - if (json.translationArgs && Array.isArray(json.translationArgs)) { - json.translationArgs.forEach((arg, index) => { - displayMessage = displayMessage.replace(`{${index}}`, arg); - }); - } - } + // If translation info is available, use MessageFormatter to translate + if (json.translationKey && window.MessageFormatter) { + displayMessage = window.MessageFormatter.translate( + json.translationKey, + json.translationArgs, + json.message // fallback to original message + ); } showErrorBanner((json.error || 'Error') + ': ' + displayMessage, json.trace || ''); diff --git a/stirling-pdf/src/main/resources/static/js/messageFormatter.js b/stirling-pdf/src/main/resources/static/js/messageFormatter.js new file mode 100644 index 000000000..cf86b4aaf --- /dev/null +++ b/stirling-pdf/src/main/resources/static/js/messageFormatter.js @@ -0,0 +1,66 @@ +/** + * Utility for formatting internationalized messages with placeholder replacement. + * Supports the {0}, {1}, {2}... placeholder format used by Java MessageFormat. + */ +window.MessageFormatter = (function() { + 'use strict'; + + /** + * Format a message template by replacing {0}, {1}, etc. placeholders with provided arguments. + * + * @param {string} template - The message template with {0}, {1}, etc. placeholders + * @param {Array|string} args - Arguments to replace placeholders with. Can be array or individual arguments + * @returns {string} The formatted message with placeholders replaced + * + * @example + * formatMessage("Hello {0}, you have {1} messages", ["John", 5]) + * // Returns: "Hello John, you have 5 messages" + * + * formatMessage("Error {0}: {1}", "404", "Not Found") + * // Returns: "Error 404: Not Found" + */ + function formatMessage(template, ...args) { + if (!template || typeof template !== 'string') { + return template || ''; + } + + // Handle case where first argument is an array + const argumentArray = Array.isArray(args[0]) ? args[0] : args; + + // Replace {0}, {1}, {2}, etc. with corresponding arguments + return template.replace(/\{(\d+)\}/g, function(match, index) { + const argIndex = parseInt(index, 10); + return argumentArray[argIndex] !== undefined && argumentArray[argIndex] !== null + ? String(argumentArray[argIndex]) + : match; // Keep original placeholder if no argument provided + }); + } + + /** + * Translate and format an error message using the global translation object. + * Falls back to the provided fallback message if translation not found. + * + * @param {string} translationKey - The translation key (e.g., "error.dpiExceedsLimit") + * @param {Array} translationArgs - Arguments for placeholder replacement + * @param {string} fallbackMessage - Fallback message if translation not found + * @returns {string} The translated and formatted message + */ + function translateAndFormat(translationKey, translationArgs, fallbackMessage) { + if (!window.stirlingPDF || !window.stirlingPDF.translations) { + return fallbackMessage || translationKey; + } + + const template = window.stirlingPDF.translations[translationKey]; + if (!template) { + return fallbackMessage || translationKey; + } + + return formatMessage(template, translationArgs || []); + } + + // Public API + return { + format: formatMessage, + translate: translateAndFormat + }; +})(); \ No newline at end of file diff --git a/stirling-pdf/src/main/resources/templates/fragments/common.html b/stirling-pdf/src/main/resources/templates/fragments/common.html index c7b9b56bb..47cafff5c 100644 --- a/stirling-pdf/src/main/resources/templates/fragments/common.html +++ b/stirling-pdf/src/main/resources/templates/fragments/common.html @@ -86,6 +86,7 @@ +