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 2c2312f2d..9d9fc514b 100644 --- a/common/src/main/java/stirling/software/common/util/ExceptionUtils.java +++ b/common/src/main/java/stirling/software/common/util/ExceptionUtils.java @@ -5,8 +5,8 @@ import java.io.IOException; import lombok.extern.slf4j.Slf4j; /** - * Utility class for handling exceptions with consistent English error messages. Frontend will - * handle translation to user's language. + * Utility class for handling exceptions with internationalized error messages. Provides consistent + * error handling and user-friendly messages across the application. */ @Slf4j public class ExceptionUtils { @@ -32,12 +32,15 @@ public class ExceptionUtils { String message; if (context != null && !context.isEmpty()) { message = - String.format( - "Error %s: PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation.", + I18nUtils.getMessage( + "error.pdfCorruptedDuring", + "Error {0}: PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation.", context); } else { message = - "PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation."; + I18nUtils.getMessage( + "error.pdfCorrupted", + "PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation."); } return new IOException(message, cause); } @@ -50,7 +53,9 @@ public class ExceptionUtils { */ public static IOException createMultiplePdfCorruptedException(Exception cause) { String message = - "One or more PDF files appear to be corrupted or damaged. Please try using the 'Repair PDF' feature on each file first before attempting to merge them."; + I18nUtils.getMessage( + "error.pdfCorruptedMultiple", + "One or more PDF files appear to be corrupted or damaged. Please try using the 'Repair PDF' feature on each file first before attempting to merge them."); return new IOException(message, cause); } @@ -62,7 +67,10 @@ public class ExceptionUtils { */ public static IOException createPdfEncryptionException(Exception cause) { String message = - "The PDF appears to have corrupted encryption data. This can happen when the PDF was created with incompatible encryption methods. Please try using the 'Repair PDF' feature first, or contact the document creator for a new copy."; + I18nUtils.getMessage( + "error.pdfEncryption", + "The PDF appears to have corrupted encryption data. This can happen when the PDF was created with incompatible encryption methods. Please try using the 'Repair PDF' feature first, or contact the document creator for a new copy.", + cause.getMessage()); return new IOException(message, cause); } @@ -74,7 +82,9 @@ public class ExceptionUtils { */ public static IOException createPdfPasswordException(Exception cause) { String message = - "The PDF Document is passworded and either the password was not provided or was incorrect"; + I18nUtils.getMessage( + "error.pdfPassword", + "The PDF Document is passworded and either the password was not provided or was incorrect"); return new IOException(message, cause); } @@ -87,74 +97,57 @@ public class ExceptionUtils { */ public static IOException createFileProcessingException(String operation, Exception cause) { String message = - String.format( - "An error occurred while processing the file during %s operation: %s", - operation, cause.getMessage()); + I18nUtils.getMessage( + "error.fileProcessing", + "An error occurred while processing the file during {0} operation: {1}", + operation, + cause.getMessage()); return new IOException(message, cause); } /** - * Create a generic IOException with readable English message. + * Create a generic IOException with internationalized message. * - * @param messageKey the i18n message key for frontend translation - * @param defaultMessage the English message template + * @param messageKey the i18n message key + * @param defaultMessage the default message if i18n is not available * @param cause the original exception - * @param args arguments for message formatting - * @return IOException with readable English message + * @param args optional arguments for the message + * @return IOException with user-friendly message */ public static IOException createIOException( String messageKey, String defaultMessage, Exception cause, Object... args) { - String message = String.format(defaultMessage, args); + String message = I18nUtils.getMessage(messageKey, defaultMessage, args); return new IOException(message, cause); } /** - * Create a generic RuntimeException with readable English message. + * Create a generic RuntimeException with internationalized message. * - * @param messageKey the i18n message key for frontend translation - * @param defaultMessage the English message template + * @param messageKey the i18n message key + * @param defaultMessage the default message if i18n is not available * @param cause the original exception - * @param args arguments for message formatting - * @return RuntimeException with readable English message + * @param args optional arguments for the message + * @return RuntimeException with user-friendly message */ public static RuntimeException createRuntimeException( String messageKey, String defaultMessage, Exception cause, Object... args) { - String message = String.format(defaultMessage, args); - if (messageKey != null) { - return new TranslatableException(message, messageKey, args); - } + String message = I18nUtils.getMessage(messageKey, defaultMessage, args); return new RuntimeException(message, cause); } /** - * Create an IllegalArgumentException with readable English message. + * Create an IllegalArgumentException with internationalized message. * - * @param messageKey the i18n message key for frontend translation - * @param defaultMessage the English message template - * @param args arguments for message formatting - * @return IllegalArgumentException with readable English message + * @param messageKey the i18n message key + * @param defaultMessage the default message if i18n is not available + * @param args optional arguments for the message + * @return IllegalArgumentException with user-friendly message */ public static IllegalArgumentException createIllegalArgumentException( String messageKey, String defaultMessage, Object... args) { - String message = String.format(defaultMessage, args); - 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); + String message = I18nUtils.getMessage(messageKey, defaultMessage, args); + System.out.println("######## Test " + message); + return new IllegalArgumentException(message); } /** Create file validation exceptions. */ @@ -319,7 +312,7 @@ public class ExceptionUtils { public static void logException(String operation, Exception e) { if (e instanceof IOException && PdfErrorUtils.isCorruptedPdfError((IOException) e)) { log.warn("PDF corruption detected during {}: {}", operation, e.getMessage()); - } else if (e instanceof IOException io && (isEncryptionError(io) || isPasswordError(io))) { + } else if (isEncryptionError((IOException) e) || isPasswordError((IOException) e)) { log.info("PDF security issue during {}: {}", operation, e.getMessage()); } else { log.error("Unexpected error during {}", operation, e); diff --git a/common/src/main/java/stirling/software/common/util/TranslatableException.java b/common/src/main/java/stirling/software/common/util/TranslatableException.java deleted file mode 100644 index 00467e330..000000000 --- a/common/src/main/java/stirling/software/common/util/TranslatableException.java +++ /dev/null @@ -1,25 +0,0 @@ -package stirling.software.common.util; - -/** - * Exception that carries translation information for frontend internationalization. The - * GlobalExceptionHandler extracts this info to create structured error responses. - */ -public class TranslatableException extends IllegalArgumentException { - - private final String translationKey; - private final Object[] translationArgs; - - public TranslatableException(String message, String translationKey, Object... translationArgs) { - super(message); - this.translationKey = translationKey; - this.translationArgs = translationArgs; - } - - public String getTranslationKey() { - return translationKey; - } - - public Object[] getTranslationArgs() { - return translationArgs; - } -} diff --git a/devGuide/EXCEPTION_HANDLING_GUIDE.md b/devGuide/EXCEPTION_HANDLING_GUIDE.md deleted file mode 100644 index 32db1280a..000000000 --- a/devGuide/EXCEPTION_HANDLING_GUIDE.md +++ /dev/null @@ -1,196 +0,0 @@ -# Exception Handling Guide - -This guide shows how to use the centralized exception handling utilities for consistent error messages with frontend translation support. - -## Architecture Overview - -The system uses a **backend-frontend translation split**: -- **Backend**: Creates structured JSON error responses with translation keys and English fallbacks -- **Frontend**: Translates error messages to user's language using JavaScript - -## New Utilities - -### 1. ExceptionUtils -Creates `TranslatableException` instances with structured translation data for frontend. -Backend uses hardcoded English strings for readability. - -### 2. GlobalExceptionHandler -Converts exceptions to structured JSON responses with translation information. - -### 3. MessageFormatter.js -Frontend utility for translating error messages with placeholder replacement. - -### 4. GlobalModelAdvice -Loads error translations using Spring's MessageSource and adds them to all templates. - -## Usage Examples - -### Basic PDF Exception Handling - -**Before:** -```java -try { - // PDF operation -} catch (IOException e) { - if (PdfErrorUtils.isCorruptedPdfError(e)) { - throw new IOException("PDF file is corrupted...", e); - } - throw e; -} -``` - -**After:** -```java -try { - // PDF operation -} catch (IOException e) { - ExceptionUtils.logException("operation name", e); - throw ExceptionUtils.handlePdfException(e); -} -``` - -### Creating Specific Exception Types - -```java -// PDF corruption -throw ExceptionUtils.createPdfCorruptedException(originalException); - -// PDF corruption with context -throw ExceptionUtils.createPdfCorruptedException("during merge", originalException); - -// Multiple PDF corruption (for merge operations) -throw ExceptionUtils.createMultiplePdfCorruptedException(originalException); - -// PDF encryption issues -throw ExceptionUtils.createPdfEncryptionException(originalException); - -// File processing errors -throw ExceptionUtils.createFileProcessingException("merge", originalException); - -// Generic exceptions with i18n -throw ExceptionUtils.createIOException("error.customKey", "Default message", originalException, arg1, arg2); -``` - -### JSON Error Response Format - -The system returns structured JSON error responses with translation support: - -```json -{ - "error": "Bad Request", - "message": "DPI value 500 exceeds maximum safe limit of 300. High DPI values can cause memory issues and crashes. Please use a lower DPI value.", - "trace": "java.lang.IllegalArgumentException: ...", - "translationKey": "error.dpiExceedsLimit", - "translationArgs": ["500", "300"] -} -``` - -**Key Features:** -- `message`: Readable English hardcoded in backend for API consumers -- `translationKey`: Frontend translation key -- `translationArgs`: Arguments for placeholder replacement -- Backend sends English, frontend translates to user's language - -### Frontend Translation with MessageFormatter - -```javascript -// Frontend translates using Spring's loaded translation data -const displayMessage = window.MessageFormatter.translate( - json.translationKey, - json.translationArgs, - json.message // English fallback -); -``` - -## Controller Pattern - -```java -@RestController -public class MyController { - - private final CustomPDFDocumentFactory pdfDocumentFactory; - - @PostMapping("/process") - public ResponseEntity processFile(@ModelAttribute FileRequest request) throws IOException { - try { - PDDocument document = pdfDocumentFactory.load(request.getFileInput()); - - // Process document... - - return WebResponseUtils.pdfDocToWebResponse(document, "output.pdf"); - } catch (IOException e) { - ExceptionUtils.logException("file processing", e); - throw ExceptionUtils.handlePdfException(e, "during processing"); - } - } -} -``` - -## Error Message Keys - -When creating new exception messages, add the corresponding i18n keys to `messages_en_GB.properties` only. The translation scripts will automatically propagate them to other language files during merge. - -### Key Categories Available: - -**Core PDF Operations:** -- `error.pdfCorrupted` - General PDF corruption -- `error.pdfCorruptedDuring` - Corruption with context (takes operation parameter) -- `error.pdfEncryption` - Encryption/decryption issues -- `error.pdfPassword` - Password-related errors - -**File Processing:** -- `error.fileProcessing` - Generic file operation errors (takes operation and error message) -- `error.commandFailed` - External tool failures (takes tool name) - -**Validation:** -- `error.invalidArgument` - Invalid parameters (takes argument description) -- `error.invalidFormat` - Invalid file formats (takes format type) -- `error.optionsNotSpecified` - Missing required options (takes option type) - -**System Requirements:** -- `error.toolNotInstalled` - Missing tools (takes tool name) -- `error.toolRequired` - Tool requirements (takes tool and operation) - -### Creating New Keys: -When adding new error scenarios, follow the naming pattern: -- `error.[category].[specific]` (e.g., `error.ocr.languageRequired`) -- Keep parameter placeholders simple and translatable -- Avoid putting full sentences in `{0}` parameters - -### Parameter Best Practices: - -**✅ Good Examples:** -```java -// Simple identifiers or values -ExceptionUtils.createIllegalArgumentException("error.invalidArgument", "Invalid argument: {0}", "angle"); -ExceptionUtils.createRuntimeException("error.commandFailed", "{0} command failed", null, "Tesseract"); -ExceptionUtils.createIllegalArgumentException("error.invalidFormat", "Invalid {0} format", "PDF"); -``` - -**❌ Bad Examples:** -```java -// Full sentences that can't be translated -ExceptionUtils.createIllegalArgumentException("error.invalidArgument", "Invalid argument: {0}", "angle must be multiple of 90"); -``` - -**Solution for Complex Messages:** -Create specific i18n keys instead: -```java -// Instead of complex parameters, create specific keys -ExceptionUtils.createIllegalArgumentException("error.angleNotMultipleOf90", "Angle must be a multiple of 90"); -``` - -## Testing Error Messages - -```java -@Test -public void testErrorMessageLocalization() { - // Test with different locales - LocaleContextHolder.setLocale(Locale.FRENCH); - - IOException exception = ExceptionUtils.createPdfCorruptedException(new RuntimeException("test")); - - // Verify message is in French - assertThat(exception.getMessage()).contains("PDF"); -} -``` \ No newline at end of file diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/config/GlobalExceptionHandler.java b/stirling-pdf/src/main/java/stirling/software/SPDF/config/GlobalExceptionHandler.java deleted file mode 100644 index c4bbf08c1..000000000 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/config/GlobalExceptionHandler.java +++ /dev/null @@ -1,94 +0,0 @@ -package stirling.software.SPDF.config; - -import java.util.Arrays; -import java.util.List; - -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.bind.annotation.ExceptionHandler; - -import lombok.extern.slf4j.Slf4j; - -/** - * Global exception handler that creates structured error responses with translation information for - * frontend internationalization support. - */ -@ControllerAdvice -@Slf4j -public class GlobalExceptionHandler { - - public static class ErrorResponse { - public String error; - public String message; - public String trace; - public String translationKey; - public List translationArgs; - - public ErrorResponse( - String error, - String message, - String trace, - String translationKey, - List translationArgs) { - this.error = error; - this.message = message; - this.trace = trace; - this.translationKey = translationKey; - this.translationArgs = translationArgs; - } - } - - @ExceptionHandler(stirling.software.common.util.TranslatableException.class) - public ResponseEntity handleTranslatableException( - stirling.software.common.util.TranslatableException e) { - List translationArgs = null; - if (e.getTranslationArgs() != null) { - translationArgs = Arrays.stream(e.getTranslationArgs()).map(String::valueOf).toList(); - } - - ErrorResponse errorResponse = - new ErrorResponse( - "Bad Request", - e.getMessage(), - getStackTrace(e), - e.getTranslationKey(), - translationArgs); - - return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); - } - - @ExceptionHandler(IllegalArgumentException.class) - public ResponseEntity handleIllegalArgumentException( - IllegalArgumentException e) { - ErrorResponse errorResponse = - new ErrorResponse("Bad Request", e.getMessage(), getStackTrace(e), null, null); - - return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); - } - - @ExceptionHandler(RuntimeException.class) - public ResponseEntity handleRuntimeException(RuntimeException e) { - ErrorResponse errorResponse = - new ErrorResponse( - "Internal Server Error", e.getMessage(), getStackTrace(e), null, null); - - return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); - } - - @ExceptionHandler(Exception.class) - public ResponseEntity handleGenericException(Exception e) { - ErrorResponse errorResponse = - new ErrorResponse( - "Internal Server Error", e.getMessage(), getStackTrace(e), null, null); - - return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); - } - - private String getStackTrace(Exception e) { - if (e.getCause() != null) { - return e.getCause().toString(); - } - return e.getClass().getSimpleName() + ": " + e.getMessage(); - } -} diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/TranslationController.java b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/TranslationController.java deleted file mode 100644 index 606a49025..000000000 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/TranslationController.java +++ /dev/null @@ -1,61 +0,0 @@ -package stirling.software.SPDF.controller.api; - -import java.util.Arrays; - -import org.springframework.context.MessageSource; -import org.springframework.context.i18n.LocaleContextHolder; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -/** - * API endpoint for on-demand error message translation. - * Provides translations for error messages when needed instead of pre-loading all translations. - */ -@RestController -@RequestMapping("/api") -@RequiredArgsConstructor -@Slf4j -public class TranslationController { - - private final MessageSource messageSource; - - /** - * Get translated error message for user's locale. - * - * @param key the translation key (e.g. "error.dpiExceedsLimit") - * @param args comma-separated arguments for message formatting - * @return translated message in user's locale - */ - @GetMapping("/translate") - public ResponseEntity translate( - @RequestParam String key, - @RequestParam(required = false) String args) { - - try { - Object[] messageArgs = null; - if (args != null && !args.trim().isEmpty()) { - messageArgs = Arrays.stream(args.split(",")) - .map(String::trim) - .toArray(); - } - - String translatedMessage = messageSource.getMessage( - key, - messageArgs, - LocaleContextHolder.getLocale() - ); - - return ResponseEntity.ok(translatedMessage); - - } catch (Exception e) { - log.debug("Translation failed for key '{}': {}", key, e.getMessage()); - return ResponseEntity.notFound().build(); - } - } -} \ No newline at end of file diff --git a/stirling-pdf/src/main/resources/static/js/downloader.js b/stirling-pdf/src/main/resources/static/js/downloader.js index 291c31084..c5a1a91db 100644 --- a/stirling-pdf/src/main/resources/static/js/downloader.js +++ b/stirling-pdf/src/main/resources/static/js/downloader.js @@ -330,9 +330,7 @@ async function handleJsonResponse(response) { const json = await response.json(); - - // Check for password-related errors first - const errorMessage = json.message || ''; + const errorMessage = JSON.stringify(json, null, 2); if ( errorMessage.toLowerCase().includes('the password is incorrect') || errorMessage.toLowerCase().includes('Password is not provided') || @@ -342,24 +340,9 @@ firstErrorOccurred = true; alert(pdfPasswordPrompt); } - return; - } - - // Handle structured error response with async translation - let displayMessage = 'Loading...'; // Brief loading state - - if (json.translationKey && window.MessageFormatter) { - // Async translation with timeout and English fallback - displayMessage = await window.MessageFormatter.translateAsync( - json.translationKey, - json.translationArgs, - json.message // English fallback - ); } else { - displayMessage = json.message; // Direct English message + showErrorBanner(json.error + ':' + json.message, json.trace); } - - showErrorBanner((json.error || 'Error') + ': ' + displayMessage, json.trace || ''); } async function handleResponse(blob, filename, considerViewOptions = false, isZip = false) { diff --git a/stirling-pdf/src/main/resources/static/js/messageFormatter.js b/stirling-pdf/src/main/resources/static/js/messageFormatter.js deleted file mode 100644 index f92d394b8..000000000 --- a/stirling-pdf/src/main/resources/static/js/messageFormatter.js +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Async translation utility with timeout and English fallback. - * Fetches translations on-demand from API with brief loading state. - */ -window.MessageFormatter = (function() { - 'use strict'; - - /** - * Translate error message with async API call and fallback to English. - * Shows brief loading, attempts translation, falls back to English on timeout/error. - * - * @param {string} translationKey - The translation key - * @param {Array} translationArgs - Arguments for message formatting - * @param {string} fallbackMessage - English fallback message - * @param {number} timeout - Timeout in milliseconds (default: 500ms) - * @returns {Promise} - Translated message or English fallback - */ - async function translateAsync(translationKey, translationArgs, fallbackMessage, timeout = 500) { - if (!translationKey) { - return fallbackMessage; - } - - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), timeout); - - try { - const params = new URLSearchParams({ key: translationKey }); - if (translationArgs && translationArgs.length > 0) { - params.append('args', translationArgs.join(',')); - } - - const response = await fetch(`${window.stirlingPDF.translationApiUrl}?${params}`, { - signal: controller.signal - }); - - clearTimeout(timeoutId); - - if (!response.ok) { - throw new Error(`Translation API returned ${response.status}`); - } - - return await response.text(); - } catch (error) { - clearTimeout(timeoutId); - console.debug('Translation failed, using English fallback:', error.message); - return fallbackMessage; - } - } - - return { translateAsync }; -})(); \ 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 6009be702..26a7971ff 100644 --- a/stirling-pdf/src/main/resources/templates/fragments/common.html +++ b/stirling-pdf/src/main/resources/templates/fragments/common.html @@ -86,7 +86,6 @@ -