Messages & Internationalization (i18n)
Overview: This document explains how messages, validation errors, and internationalization are configured in Smart Supply Pro.
Table of Contents
- Message Properties Overview
- Supported Languages
- Validation Messages
- Error Codes & Messages
- Internationalization Strategy
- Frontend Integration
Message Properties Overview
Purpose
Message properties files centralize text content used throughout the application: - Validation error messages - Error response messages - Business exception descriptions - User-facing error codes
Current Implementation Status
Status: Infrastructure prepared, implementation ongoing
src/main/resources/
├── messages.properties # Default/English messages
├── messages_de.properties # German messages
└── (more languages as needed)
What’s Externalized: - ✅ Validation constraint messages (JSR-380) - ✅ Custom validator error messages - ✅ GlobalExceptionHandler response messages - ✅ Error codes for frontend error handling
Why Externalize Messages?
Without externalization (bad):
if (item.getQuantity() < 0) {
throw new InvalidParameterException("Quantity cannot be negative");
}
// Problem: Message hardcoded; can't translate; difficult to maintainWith externalization (good):
if (item.getQuantity() < 0) {
throw new InvalidParameterException(messageSource.getMessage(
"inventory.item.quantity.negative", null, locale));
}
// Message stored in: messages.properties (EN), messages_de.properties (DE)
// Automatically returns correct translation based on localeSupported Languages
Language Configuration
| Language | File | Locale Code | Status |
|---|---|---|---|
| English | messages.properties |
en |
✅ Base language |
| German | messages_de.properties |
de |
✅ Supported |
How Locale is Determined
Server-side (HTTP requests):
Spring checks (in order):
1. URL parameter: ?lang=de
2. Cookie: SPRING_LOCALE
3. Accept-Language header: Accept-Language: de-DE
4. Server default (locale.setDefaultLocale())
Example:
# Request with German locale
curl "http://localhost:8081/api/suppliers?lang=de"
# Response messages will be in German
# Using Accept-Language header
curl -H "Accept-Language: de-DE" http://localhost:8081/api/suppliers
# Response messages will be in GermanClient-side (Frontend React):
// Frontend controls locale independently
const [locale, setLocale] = useState('en'); // or 'de'
// Frontend sends request with locale
const response = await fetch('/api/suppliers?lang=' + locale);Validation Messages
JSR-380 Annotation Messages
Validation constraints can specify message keys:
public class InventoryItemDTO {
@NotBlank(message = "{inventory.item.name.required}")
private String name;
@Min(value = 0, message = "{inventory.item.quantity.min}")
private Integer quantity;
@Email(message = "{inventory.supplier.email.invalid}")
private String supplierEmail;
}Message Properties Files
messages.properties (English):
# Inventory Item Validation
inventory.item.name.required=Item name is required
inventory.item.name.too.long=Item name cannot exceed 255 characters
inventory.item.quantity.min=Quantity must be at least 0
inventory.item.quantity.max=Quantity cannot exceed 999999
inventory.item.price.min=Price must be greater than 0
inventory.item.price.precision=Price can have at most 2 decimal places
# Supplier Validation
inventory.supplier.email.invalid=Invalid email address
inventory.supplier.name.required=Supplier name is required
messages_de.properties (German):
# Lagerverwaltung Validierung
inventory.item.name.required=Artikelname ist erforderlich
inventory.item.name.too.long=Artikelname darf 255 Zeichen nicht überschreiten
inventory.item.quantity.min=Menge muss mindestens 0 sein
inventory.item.quantity.max=Menge darf 999999 nicht überschreiten
inventory.item.price.min=Preis muss größer als 0 sein
inventory.item.price.precision=Preis darf höchstens 2 Dezimalstellen haben
# Lieferant Validierung
inventory.supplier.email.invalid=Ungültige E-Mail-Adresse
inventory.supplier.name.required=Lieferantenname ist erforderlich
Custom Validator Messages
// Custom validator class
@Component
public class InventoryItemValidator {
private final MessageSource messageSource;
public InventoryItemValidator(MessageSource messageSource) {
this.messageSource = messageSource;
}
public void validateInventoryItemNotExists(String name, BigDecimal price, Locale locale) {
if (inventoryItemRepository.findByNameIgnoreCase(name).isPresent()) {
String message = messageSource.getMessage(
"inventory.item.duplicate", // Key from properties
null, // Arguments (if parameterized)
locale // Locale
);
throw new DuplicateResourceException(message);
}
}
}messages.properties:
inventory.item.duplicate=An inventory item with this name already exists
messages_de.properties:
inventory.item.duplicate=Ein Artikel mit diesem Namen existiert bereits
Error Codes & Messages
Exception Response Structure
GlobalExceptionHandler returns:
{
"error": "DUPLICATE_RESOURCE",
"message": "An inventory item with this name already exists",
"timestamp": "2024-11-20T14:30:00Z",
"path": "/api/suppliers",
"status": 409
}Error Code Mapping
messages.properties:
# Error codes and descriptions
error.duplicate.resource=An inventory item with this name already exists
error.not.found.supplier=Supplier not found
error.invalid.parameter=Invalid parameter provided
error.unauthorized=You are not authorized to perform this action
error.forbidden=Access to this resource is forbidden
error.internal.server.error=An unexpected error occurred
messages_de.properties:
error.duplicate.resource=Ein Artikel mit diesem Namen existiert bereits
error.not.found.supplier=Lieferant nicht gefunden
error.invalid.parameter=Ungültiger Parameter angegeben
error.unauthorized=Sie sind nicht berechtigt, diese Aktion durchzuführen
error.forbidden=Zugriff auf diese Ressource ist nicht gestattet
error.internal.server.error=Ein unerwarteter Fehler ist aufgetreten
Usage in Exception Handler
@RestControllerAdvice
public class GlobalExceptionHandler {
private final MessageSource messageSource;
@ExceptionHandler(DuplicateResourceException.class)
public ResponseEntity<ErrorResponse> handleDuplicateResource(
DuplicateResourceException ex,
HttpServletRequest request,
Locale locale) {
String message = messageSource.getMessage(
"error.duplicate.resource",
null,
locale
);
return ResponseEntity.status(409).body(
new ErrorResponse(
"DUPLICATE_RESOURCE",
message,
LocalDateTime.now(),
request.getRequestURI(),
409
)
);
}
}Internationalization Strategy
Message Key Naming Convention
Pattern:
{domain}.{entity}.{aspect}.{detail}
Examples:
inventory.item.name.required
├── domain: inventory (application domain)
├── entity: item (what entity)
├── aspect: name (which field or concern)
└── detail: required (specific message)
inventory.supplier.email.invalid
└── validation error for supplier email
error.not.found.supplier
├── domain: error (error message)
├── aspect: not (what happened)
└── detail: found (what)
Message File Organization
Flat structure (current):
messages.properties
├── inventory.item.*
├── inventory.supplier.*
├── error.*
└── (all messages in one file)
Alternative hierarchical (for larger apps):
messages/
├── inventory/
│ ├── item.properties
│ └── supplier.properties
├── error/
│ └── messages.properties
└── common.properties
Adding New Languages
To add French (fr):
- Create file:
touch src/main/resources/messages_fr.properties- Add translations:
# messages_fr.properties
inventory.item.name.required=Le nom de l'article est requis
inventory.item.quantity.min=La quantité doit être au moins 0
- Clients can use:
curl "http://localhost:8081/api/suppliers?lang=fr"Frontend Integration
Fetching Messages from Backend
Option 1: Get all messages at once
// frontend/src/services/i18n.ts
export async function fetchMessages(locale: string) {
const response = await fetch(`/api/messages?lang=${locale}`);
return response.json();
// Returns: { "inventory.item.name.required": "Item name is required", ... }
}Backend endpoint (if implemented):
@GetMapping("/api/messages")
public Map<String, String> getMessages(@RequestParam String lang) {
Locale locale = new Locale(lang);
// Return all messages for locale
return messageService.getAllMessages(locale);
}Option 2: Include messages in error response
{
"error": "VALIDATION_ERROR",
"message": "validation.inventory.item.quantity.min",
"localizedMessage": "Quantity must be at least 0",
"status": 400
}Frontend reads:
const error = response.data;
// Display: error.localizedMessage
// Or: fetch translation using error.message keyFrontend Localization Example
React component with i18n:
// frontend/src/i18n/messages.ts
const messages = {
en: {
'inventory.item.name.required': 'Item name is required',
'inventory.item.quantity.min': 'Quantity must be at least 0',
'error.duplicate.resource': 'An item with this name already exists',
},
de: {
'inventory.item.name.required': 'Artikelname ist erforderlich',
'inventory.item.quantity.min': 'Menge muss mindestens 0 sein',
'error.duplicate.resource': 'Ein Artikel mit diesem Namen existiert bereits',
}
};
// Component usage
function useMessage(key: string, locale: string = 'en') {
return messages[locale]?.[key] || key;
}
// In component
export function SupplierForm() {
const [locale, setLocale] = useState('en');
return (
<div>
<label>{useMessage('inventory.item.name.required', locale)}</label>
<input type="text" placeholder="Enter item name" />
</div>
);
}Alternatively: Use Frontend i18n Library
Popular options: - i18next - Most popular, mature - React Intl - Format dates, numbers, pluralization - Format.js - Lightweight, small bundle
Example with i18next:
import i18n from 'i18next';
i18n.init({
resources: {
en: {
translation: {
'inventory.item.name': 'Item Name',
'inventory.item.quantity': 'Quantity'
}
},
de: {
translation: {
'inventory.item.name': 'Artikelname',
'inventory.item.quantity': 'Menge'
}
}
}
});
// In React component
<h1>{t('inventory.item.name')}</h1>Current Status & Next Steps
Currently Implemented
✅ Message properties files created ✅ Validation message keys defined ✅ Error code messages externalized ✅ German translations available
Next Steps (Future Enhancement)
Best Practices
1. Always Externalize User-Facing Messages
❌ Bad:
throw new InvalidParameterException("Quantity must be positive");✅ Good:
String message = messageSource.getMessage("inventory.item.quantity.positive", null, locale);
throw new InvalidParameterException(message);2. Use Consistent Key Naming
❌ Bad:
item_name_error=Item name is required
ItemNameRequired=Item name is required
nameRequired=Item name is required
✅ Good:
inventory.item.name.required=Item name is required
3. Keep Message Files Small & Organized
❌ Bad: 1000+ messages in single file
✅ Good: Group by domain/entity
messages.properties (base English)
messages_de.properties (German translation)
messages_fr.properties (French translation, if added)
4. Provide Context in Messages
❌ Bad:
error=An error occurred
✅ Good:
error.duplicate.resource=An inventory item with this name already exists
error.not.found.supplier=Supplier with ID {0} not found
5. Parameterize Messages When Needed
❌ Bad: Create separate messages for each ID/name
supplier.not.found.1=Supplier with ID 1 not found
supplier.not.found.2=Supplier with ID 2 not found
✅ Good: Use parameterized message
supplier.not.found=Supplier with ID {0} not found
// Usage
messageSource.getMessage("supplier.not.found", new Object[]{supplierId}, locale)
Summary
| Aspect | Details |
|---|---|
| File Format | Java .properties (key=value format) |
| Base Language | English (messages.properties) |
| Supported Languages | English, German (more can be added) |
| Location | src/main/resources/ |
| Framework | Spring’s MessageSource |
| Usage | Validation messages, error responses, user-facing text |
| Frontend Integration | React can fetch and display localized messages |