Add i18n module: MessageSource, LocaleResolver and localized ProblemDetail

English, Marathi and Hindi bundles behind one small web app, with tests that run it on a real
port and write the transcripts (01-11) quoted in the article, plus a jar-metadata capture (12).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Uu7q8vPeREyT4218EJPzz1
This commit is contained in:
Claude
2026-09-24 09:54:55 +00:00
parent e59ff029a1
commit 2cbf31e7d5
36 changed files with 1182 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
# GET /api/greeting under different Accept-Language headers (Spring Boot's default LocaleResolver)
Accept-Language: mr-IN -> status=200 locale=mr_IN message=नमस्कार, Asha!
Accept-Language: hi -> status=200 locale=hi message=नमस्ते, Asha!
Accept-Language: mr;q=0.4, hi;q=0.9 -> status=200 locale=hi message=नमस्ते, Asha!
Accept-Language: en-GB -> status=200 locale=en_GB message=Hello, Asha!
Accept-Language: fr -> status=200 locale=fr message=Hello, Asha!
Accept-Language: (no header) -> status=200 locale=en_US message=Hello, Asha!
@@ -0,0 +1,11 @@
# The JVM's default locale is Hindi: what does a French or an English request get?
Locale.getDefault() while these ran: hi
--- spring.messages.fallback-to-system-locale left at its default ---
Accept-Language: fr -> status=200 locale=fr message=नमस्ते, Asha!
Accept-Language: en -> status=200 locale=en message=नमस्ते, Asha!
--- spring.messages.fallback-to-system-locale=false ---
Accept-Language: fr -> status=200 locale=fr message=Hello, Asha!
Accept-Language: en -> status=200 locale=en message=Hello, Asha!
+20
View File
@@ -0,0 +1,20 @@
# Letting the user pick a language: ?lang=mr with a cookie resolver, and two ways to get it wrong
--- A. CookieLocaleResolver named localeResolver + LocaleChangeInterceptor (profile 'cookie') ---
no cookie yet, Accept-Language: hi -> status=200 locale=en message=Hello, Asha!
?lang=mr, Accept-Language: hi -> status=200 locale=mr message=नमस्कार, Asha!
Set-Cookie: lang=mr; Path=/; Max-Age=2592000; SameSite=Lax
Cookie: lang=mr, Accept-Language: hi -> status=200 locale=mr message=नमस्कार, Asha!
--- B. the same resolver under the bean name cookieLocaleResolver (profile 'cookiewrongname') ---
Accept-Language: hi -> status=200 locale=hi message=नमस्ते, Asha!
?lang=mr -> status=500 (Tomcat's own HTML error page)
bean 'cookieLocaleResolver' exists: true
bean 'localeResolver' is a: AcceptHeaderLocaleResolver
what the interceptor triggers: UnsupportedOperationException: Cannot change HTTP Accept-Language header - use a different locale resolution strategy
--- C. only the interceptor, Spring Boot's default resolver (profile 'interceptoronly') ---
?lang=mr -> status=500 (Tomcat's own HTML error page)
bean 'localeResolver' is a: AcceptHeaderLocaleResolver
what the interceptor triggers: UnsupportedOperationException: Cannot change HTTP Accept-Language header - use a different locale resolution strategy
@@ -0,0 +1,9 @@
# The same sentence with a doubled and a single apostrophe, with and without arguments
apos.doubled = Order {0} can''t be shipped yet.
apos.single = Order {0} can't be shipped yet.
with an argument: doubled -> Order A-1 can't be shipped yet.
with an argument: single -> Order A-1 cant be shipped yet.
without arguments: doubled -> Order {0} can''t be shipped yet.
without arguments: single -> Order {0} can't be shipped yet.
+11
View File
@@ -0,0 +1,11 @@
# GET /api/cart?items=1234567 in three languages, and two ways to keep Latin digits
Accept-Language: en -> You have 1,234,567 items in your cart.
Accept-Language: hi -> आपकी कार्ट में 1,234,567 वस्तुएँ हैं।
Accept-Language: mr -> तुमच्या कार्टमध्ये १,२३४,५६७ वस्तू आहेत.
--- fix 1: the client asks for Latin digits (Unicode extension -u-nu-latn) ---
Accept-Language: mr-IN-u-nu-latn -> locale=mr_IN_#u-nu-latn तुमच्या कार्टमध्ये 1,234,567 वस्तू आहेत.
--- fix 2: the server formats the number itself and passes text (GET /api/cart-latin) ---
Accept-Language: mr -> तुमच्या कार्टमध्ये 1,234,567 वस्तू आहेत.
+4
View File
@@ -0,0 +1,4 @@
# A key that exists only in English, and a key that exists nowhere
only.english.key, locale mr -> This sentence has not been translated yet.
no.such.key, locale mr -> org.springframework.context.NoSuchMessageException: No message found under code 'no.such.key' for locale 'mr'.
+4
View File
@@ -0,0 +1,4 @@
# The same Marathi greeting, with Spring Boot's default encoding and with spring.messages.encoding=ISO-8859-1
default (UTF-8): नमस्कार, Asha!
ISO-8859-1: नमस्कार, Asha!
+39
View File
@@ -0,0 +1,39 @@
# POST /api/customers with an invalid body, four Accept-Language values
body: {"name":"","email":"x","age":10}
--- Accept-Language: en ---
status=400 Content-Type=application/problem+json
title: Validation failed
detail: The request has invalid fields.
error: age: You must be at least 18 years old.
error: city: must not be null
error: email: Enter a valid email address.
error: name: Name is required.
--- Accept-Language: mr ---
status=400 Content-Type=application/problem+json
title: पडताळणी अयशस्वी
detail: विनंतीतील काही फील्ड चुकीची आहेत.
error: age: तुमचे वय किमान 18 वर्षे असणे आवश्यक आहे.
error: city: रिक्त नसावे.
error: email: कृपया वैध ईमेल पत्ता द्या.
error: name: नाव आवश्यक आहे.
--- Accept-Language: hi ---
status=400 Content-Type=application/problem+json
title: सत्यापन विफल
detail: अनुरोध के कुछ फ़ील्ड अमान्य हैं।
error: age: आपकी आयु कम से कम 18 वर्ष होनी चाहिए।
error: city: खाली नहीं होना चाहिए।
error: email: कृपया सही ईमेल पता दर्ज करें।
error: name: नाम आवश्यक है।
--- Accept-Language: fr ---
status=400 Content-Type=application/problem+json
title: Validation failed
detail: The request has invalid fields.
error: age: You must be at least 18 years old.
error: city: ne doit pas être nul
error: email: Enter a valid email address.
error: name: Name is required.
@@ -0,0 +1,44 @@
# Three kinds of exception: an ErrorResponse, an ordinary exception handled through the MessageSource, and one handled with English text in the source
--- an ErrorResponse (OrderNotFoundException) ---
Accept-Language: en
status=404 Content-Type=application/problem+json
title: Order not found
detail: No order with id 7 exists.
Accept-Language: mr
status=404 Content-Type=application/problem+json
title: ऑर्डर सापडली नाही
detail: ७ क्रमांकाची ऑर्डर अस्तित्वात नाही.
Accept-Language: hi
status=404 Content-Type=application/problem+json
title: ऑर्डर नहीं मिला
detail: क्रमांक 7 वाला ऑर्डर मौजूद नहीं है।
--- an ordinary exception, translated in the handler (PaymentDeclinedException) ---
Accept-Language: en
status=402 Content-Type=application/problem+json
title: Payment declined
detail: Payment of 499.00 was declined.
Accept-Language: mr
status=402 Content-Type=application/problem+json
title: पेमेंट नाकारले
detail: 499.00 चे पेमेंट नाकारले गेले.
Accept-Language: hi
status=402 Content-Type=application/problem+json
title: भुगतान अस्वीकृत
detail: 499.00 का भुगतान अस्वीकार कर दिया गया।
--- an ordinary exception, English in the handler (LegacyStateException) ---
Accept-Language: en
status=409 Content-Type=application/problem+json
title: Conflict
detail: The order is in a state that does not allow this.
Accept-Language: mr
status=409 Content-Type=application/problem+json
title: Conflict
detail: The order is in a state that does not allow this.
Accept-Language: hi
status=409 Content-Type=application/problem+json
title: Conflict
detail: The order is in a state that does not allow this.
@@ -0,0 +1,6 @@
# The same four lookups with spring.messages.always-use-message-format=true
with an argument: doubled -> Order A-1 can't be shipped yet.
with an argument: single -> Order A-1 cant be shipped yet.
without arguments: doubled -> Order {0} can't be shipped yet.
without arguments: single -> Order {0} cant be shipped yet.
+12
View File
@@ -0,0 +1,12 @@
# spring.web.locale on its own, and with spring.web.locale-resolver=fixed
--- spring.web.locale=hi (spring.web.locale-resolver left at accept-header) ---
Accept-Language: (no header) -> status=200 locale=hi message=नमस्ते, Asha!
Accept-Language: mr -> status=200 locale=mr message=नमस्कार, Asha!
Accept-Language: fr -> status=200 locale=fr message=Hello, Asha!
--- spring.web.locale=hi and spring.web.locale-resolver=fixed ---
Accept-Language: (no header) -> status=200 locale=hi message=नमस्ते, Asha!
Accept-Language: mr -> status=200 locale=hi message=नमस्ते, Asha!
Accept-Language: fr -> status=200 locale=hi message=नमस्ते, Asha!
+31
View File
@@ -0,0 +1,31 @@
# spring.messages.* and spring.web.locale* in the configuration metadata shipped with Spring Boot 4.1.1
jar: spring-boot-autoconfigure-4.1.1.jar
spring.messages.always-use-message-format
type: java.lang.Boolean
default: false
spring.messages.basename
type: java.util.List<java.lang.String>
default: ["messages"]
spring.messages.cache-duration
type: java.time.Duration
default: (none)
spring.messages.common-messages
type: java.util.List<org.springframework.core.io.Resource>
default: (none)
spring.messages.encoding
type: java.nio.charset.Charset
default: "UTF-8"
spring.messages.fallback-to-system-locale
type: java.lang.Boolean
default: true
spring.messages.use-code-as-default-message
type: java.lang.Boolean
default: false
spring.web.locale
type: java.util.Locale
default: (none)
spring.web.locale-resolver
type: org.springframework.boot.autoconfigure.web.WebProperties$LocaleResolver
default: "accept-header"