mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 22:29:24 +00:00
changes
This commit is contained in:
parent
558e35de62
commit
ce417469d4
@ -94,35 +94,32 @@ public class ExceptionUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a generic IOException with internationalized message.
|
||||
* Create a generic IOException with readable English message.
|
||||
*
|
||||
* @param messageKey the i18n message key
|
||||
* @param defaultMessage the default message if i18n is not available
|
||||
* @param messageKey the i18n message key for frontend translation
|
||||
* @param defaultMessage the English message template
|
||||
* @param cause the original exception
|
||||
* @param args optional arguments for the message
|
||||
* @return IOException with user-friendly message
|
||||
* @param args arguments for message formatting
|
||||
* @return IOException with readable English message
|
||||
*/
|
||||
public static IOException createIOException(
|
||||
String messageKey, String defaultMessage, Exception cause, Object... args) {
|
||||
String message = messageKey != null ? defaultMessage : String.format(defaultMessage, args);
|
||||
if (messageKey != null) {
|
||||
return new TranslatableIOException(message, messageKey, cause, args);
|
||||
}
|
||||
String message = String.format(defaultMessage, args);
|
||||
return new IOException(message, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a generic RuntimeException with internationalized message.
|
||||
* Create a generic RuntimeException with readable English message.
|
||||
*
|
||||
* @param messageKey the i18n message key
|
||||
* @param defaultMessage the default message if i18n is not available
|
||||
* @param messageKey the i18n message key for frontend translation
|
||||
* @param defaultMessage the English message template
|
||||
* @param cause the original exception
|
||||
* @param args optional arguments for the message
|
||||
* @return RuntimeException with user-friendly message
|
||||
* @param args arguments for message formatting
|
||||
* @return RuntimeException with readable English message
|
||||
*/
|
||||
public static RuntimeException createRuntimeException(
|
||||
String messageKey, String defaultMessage, Exception cause, Object... args) {
|
||||
String message = messageKey != null ? defaultMessage : String.format(defaultMessage, args);
|
||||
String message = String.format(defaultMessage, args);
|
||||
if (messageKey != null) {
|
||||
return new TranslatableException(message, messageKey, args);
|
||||
}
|
||||
@ -130,17 +127,16 @@ public class ExceptionUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an IllegalArgumentException with internationalized message.
|
||||
* Create an 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
|
||||
* @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
|
||||
*/
|
||||
public static IllegalArgumentException createIllegalArgumentException(
|
||||
String messageKey, String defaultMessage, Object... args) {
|
||||
// Only format if no translation key provided (for backwards compatibility)
|
||||
String message = messageKey != null ? defaultMessage : String.format(defaultMessage, args);
|
||||
String message = String.format(defaultMessage, args);
|
||||
return new TranslatableException(message, messageKey, args);
|
||||
}
|
||||
|
||||
|
@ -1,28 +0,0 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* IOException that carries translation information for frontend internationalization. The
|
||||
* GlobalExceptionHandler extracts this info to create structured error responses.
|
||||
*/
|
||||
public class TranslatableIOException extends IOException {
|
||||
|
||||
private final String translationKey;
|
||||
private final Object[] translationArgs;
|
||||
|
||||
public TranslatableIOException(
|
||||
String message, String translationKey, Exception cause, Object... translationArgs) {
|
||||
super(message, cause);
|
||||
this.translationKey = translationKey;
|
||||
this.translationArgs = translationArgs;
|
||||
}
|
||||
|
||||
public String getTranslationKey() {
|
||||
return translationKey;
|
||||
}
|
||||
|
||||
public Object[] getTranslationArgs() {
|
||||
return translationArgs;
|
||||
}
|
||||
}
|
@ -12,6 +12,7 @@ The system uses a **backend-frontend translation split**:
|
||||
|
||||
### 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.
|
||||
@ -19,6 +20,9 @@ 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
|
||||
@ -82,19 +86,19 @@ The system returns structured JSON error responses with translation support:
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
- `message`: English fallback for API consumers that ignore translation
|
||||
- `message`: Readable English hardcoded in backend for API consumers
|
||||
- `translationKey`: Frontend translation key
|
||||
- `translationArgs`: Arguments for placeholder replacement
|
||||
- API consumers can rely on `message` for backwards compatibility
|
||||
- Backend sends English, frontend translates to user's language
|
||||
|
||||
### Frontend Translation with MessageFormatter
|
||||
|
||||
```javascript
|
||||
// Translate error messages with placeholder replacement
|
||||
// Frontend translates using Spring's loaded translation data
|
||||
const displayMessage = window.MessageFormatter.translate(
|
||||
json.translationKey,
|
||||
json.translationArgs,
|
||||
json.message // fallback to original message
|
||||
json.message // English fallback
|
||||
);
|
||||
```
|
||||
|
||||
|
@ -58,25 +58,6 @@ public class GlobalExceptionHandler {
|
||||
return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
@ExceptionHandler(stirling.software.common.util.TranslatableIOException.class)
|
||||
public ResponseEntity<ErrorResponse> handleTranslatableIOException(
|
||||
stirling.software.common.util.TranslatableIOException e) {
|
||||
List<String> 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<ErrorResponse> handleIllegalArgumentException(
|
||||
IllegalArgumentException e) {
|
||||
|
@ -1,26 +0,0 @@
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.service.TranslationService;
|
||||
|
||||
/**
|
||||
* Global controller advice that adds common model attributes to all templates. Makes translation
|
||||
* service available for client-side error message translation.
|
||||
*/
|
||||
@ControllerAdvice
|
||||
@RequiredArgsConstructor
|
||||
public class GlobalModelAdvice {
|
||||
|
||||
private final TranslationService translationService;
|
||||
|
||||
/** Add error messages to all templates for frontend translation support. */
|
||||
@ModelAttribute
|
||||
public void addErrorMessages(Model model) {
|
||||
model.addAttribute("errorMessages", translationService.getErrorMessages());
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
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<String> 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,79 +0,0 @@
|
||||
package stirling.software.SPDF.service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.context.i18n.LocaleContextHolder;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.support.PropertiesLoaderUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Service for providing translation data to frontend JavaScript. Dynamically loads all error.*
|
||||
* messages for client-side translation.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class TranslationService {
|
||||
|
||||
private final MessageSource messageSource;
|
||||
|
||||
/**
|
||||
* Get all error messages for the current locale to pass to frontend JavaScript. This allows
|
||||
* dynamic translation of error messages sent from backend.
|
||||
*
|
||||
* @return Map of error message keys to localized values
|
||||
*/
|
||||
public Map<String, String> getErrorMessages() {
|
||||
return getErrorMessages(LocaleContextHolder.getLocale());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all error messages for a specific locale.
|
||||
*
|
||||
* @param locale the locale to get messages for
|
||||
* @return Map of error message keys to localized values
|
||||
*/
|
||||
public Map<String, String> getErrorMessages(Locale locale) {
|
||||
Map<String, String> errorMessages = new HashMap<>();
|
||||
|
||||
try {
|
||||
// Load the base messages file to get all available keys
|
||||
ClassPathResource resource = new ClassPathResource("messages_en_GB.properties");
|
||||
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
|
||||
|
||||
// Filter for error.* keys and get their localized values
|
||||
for (Object keyObj : properties.keySet()) {
|
||||
String key = (String) keyObj;
|
||||
if (key.startsWith("error.")) {
|
||||
try {
|
||||
String localizedMessage = messageSource.getMessage(key, null, locale);
|
||||
errorMessages.put(key, localizedMessage);
|
||||
} catch (Exception e) {
|
||||
log.debug(
|
||||
"Could not resolve message for key '{}' in locale '{}': {}",
|
||||
key,
|
||||
locale,
|
||||
e.getMessage());
|
||||
// Fallback to the default message from properties
|
||||
errorMessages.put(key, (String) properties.get(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("Loaded {} error messages for locale '{}'", errorMessages.size(), locale);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to load error messages for locale '{}': {}", locale, e.getMessage());
|
||||
}
|
||||
|
||||
return errorMessages;
|
||||
}
|
||||
}
|
@ -345,16 +345,18 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle structured error response with translation support
|
||||
let displayMessage = json.message;
|
||||
// Handle structured error response with async translation
|
||||
let displayMessage = 'Loading...'; // Brief loading state
|
||||
|
||||
// If translation info is available, use MessageFormatter to translate
|
||||
if (json.translationKey && window.MessageFormatter) {
|
||||
displayMessage = window.MessageFormatter.translate(
|
||||
// Async translation with timeout and English fallback
|
||||
displayMessage = await window.MessageFormatter.translateAsync(
|
||||
json.translationKey,
|
||||
json.translationArgs,
|
||||
json.message // fallback to original message
|
||||
json.message // English fallback
|
||||
);
|
||||
} else {
|
||||
displayMessage = json.message; // Direct English message
|
||||
}
|
||||
|
||||
showErrorBanner((json.error || 'Error') + ': ' + displayMessage, json.trace || '');
|
||||
|
@ -1,66 +1,51 @@
|
||||
/**
|
||||
* Utility for formatting internationalized messages with placeholder replacement.
|
||||
* Supports the {0}, {1}, {2}... placeholder format used by Java MessageFormat.
|
||||
* Async translation utility with timeout and English fallback.
|
||||
* Fetches translations on-demand from API with brief loading state.
|
||||
*/
|
||||
window.MessageFormatter = (function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Format a message template by replacing {0}, {1}, etc. placeholders with provided arguments.
|
||||
* 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} 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"
|
||||
* @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<string>} - Translated message or English fallback
|
||||
*/
|
||||
function formatMessage(template, ...args) {
|
||||
if (!template || typeof template !== 'string') {
|
||||
return template || '';
|
||||
async function translateAsync(translationKey, translationArgs, fallbackMessage, timeout = 500) {
|
||||
if (!translationKey) {
|
||||
return fallbackMessage;
|
||||
}
|
||||
|
||||
// Handle case where first argument is an array
|
||||
const argumentArray = Array.isArray(args[0]) ? args[0] : args;
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
// 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
|
||||
});
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
return { translateAsync };
|
||||
})();
|
@ -251,12 +251,8 @@
|
||||
window.stirlingPDF.uploadLimit = /*[[${@uploadLimitService.getUploadLimit()}]]*/ 0;
|
||||
window.stirlingPDF.uploadLimitExceededSingular = /*[[#{uploadLimitExceededSingular}]]*/ 'is too large. Maximum allowed size is';
|
||||
window.stirlingPDF.uploadLimitExceededPlural = /*[[#{uploadLimitExceededPlural}]]*/ 'are too large. Maximum allowed size is';
|
||||
window.stirlingPDF.pdfCorruptedMessage = /*[[#{error.pdfInvalid}]]*/ 'The PDF file "{0}" appears to be corrupted or has an invalid structure. Please try using the \'Repair PDF\' feature to fix the file before proceeding.';
|
||||
window.stirlingPDF.tryRepairMessage = /*[[#{error.tryRepair}]]*/ 'Try using the Repair PDF feature to fix corrupted files.';
|
||||
|
||||
// Translation service for dynamic error message translation
|
||||
// Dynamically populated with all error.* messages for current locale
|
||||
window.stirlingPDF.translations = /*[[${errorMessages}]]*/ {};
|
||||
// Translation API base URL for async error message translation
|
||||
window.stirlingPDF.translationApiUrl = '/api/translate';
|
||||
})();
|
||||
</script>
|
||||
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
|
||||
|
Loading…
x
Reference in New Issue
Block a user