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:
@@ -0,0 +1,85 @@
|
||||
# i18n
|
||||
|
||||
Companion project for the article **[Internationalization (i18n) in Spring Boot 4: MessageSource, LocaleResolver and Localized ProblemDetail](https://ankurm.com/spring-boot-4-internationalization-messagesource-localeresolver-problemdetail/)** on **[ankurm.com](https://ankurm.com)**.
|
||||
|
||||
One small web application that answers in English, Marathi and Hindi, plus the test suite that starts it on a real
|
||||
port and records what it says. Every console block quoted in the article came out of `output/`. Transcripts 01-11 are
|
||||
written by the test suite (so a claim that stops being true turns the build red); 12 is read out of the Spring Boot jar
|
||||
by `scripts/capture-facts.sh`.
|
||||
|
||||
There is deliberately **no `docs/` folder**: the deeper material lives in collapsible "going deeper"
|
||||
sections inside the article itself, next to the paragraph each one extends.
|
||||
|
||||
## Versions
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Spring Boot | 4.1.1 |
|
||||
| Spring Framework | 7.0.9 |
|
||||
| JDK | 25 (Temurin 25.0.4.1+1) |
|
||||
| Maven | 3.9 |
|
||||
|
||||
## Quickstart
|
||||
|
||||
```bash
|
||||
export JAVA_HOME=/path/to/jdk-25
|
||||
mvn test # runs every scenario and rewrites output/01-11
|
||||
./scripts/run-all.sh # everything, including 12 (needs python3)
|
||||
```
|
||||
|
||||
The tests pin the JVM to `-Duser.language=en -Duser.country=US -Dfile.encoding=UTF-8` (see `pom.xml`) so the
|
||||
transcripts do not depend on the machine they were generated on, and run each test class in its own JVM
|
||||
(`reuseForks=false`) because Tomcat reads the JVM's default locale once.
|
||||
|
||||
## Profiles
|
||||
|
||||
| Profile | What it turns on |
|
||||
|---|---|
|
||||
| *(none)* | Spring Boot's defaults: `AcceptHeaderLocaleResolver`, basename `messages`, UTF-8 |
|
||||
| `cookie` | `CookieLocaleResolver("lang")` named `localeResolver`, plus a `LocaleChangeInterceptor` for `?lang=` |
|
||||
| `cookiewrongname` | the same resolver under the bean name `cookieLocaleResolver`, which Spring MVC ignores |
|
||||
| `interceptoronly` | the `LocaleChangeInterceptor` with Spring Boot's default resolver |
|
||||
|
||||
## Endpoints
|
||||
|
||||
| Endpoint | What it shows |
|
||||
|---|---|
|
||||
| `GET /api/greeting?name=` | `MessageSource` with a `Locale` controller argument |
|
||||
| `GET /api/cart?items=` | a number formatted by `MessageFormat` in the request locale |
|
||||
| `GET /api/cart-latin?items=` | the same sentence with the number pre-formatted as text |
|
||||
| `GET /api/orders/{id}` | `ErrorResponseException`: `problemDetail.*` keys translate it automatically |
|
||||
| `POST /api/customers` | Bean Validation messages and a localized `MethodArgumentNotValidException` |
|
||||
| `GET /api/pay` | an ordinary exception translated in the `@ExceptionHandler` |
|
||||
| `GET /api/legacy` | an ordinary exception whose handler hard-codes English |
|
||||
|
||||
## Source layout
|
||||
|
||||
| Path | What it holds |
|
||||
|---|---|
|
||||
| `src/main/resources/messages*.properties` | English, Marathi (`_mr`) and Hindi (`_hi`), UTF-8 |
|
||||
| `web/ShopController` | the endpoints above |
|
||||
| `web/CustomerRequest` | Bean Validation with `{message.key}` placeholders |
|
||||
| `web/OrderNotFoundException` | an `ErrorResponseException` with a message-code argument |
|
||||
| `web/ApiExceptionHandler` | `ResponseEntityExceptionHandler` subclass |
|
||||
| `config/LocaleConfig` | one nested configuration per profile |
|
||||
| `src/test/.../LocaleResolutionTests` | transcripts 01, 03, 11 |
|
||||
| `src/test/.../SystemLocaleFallbackTests` | transcript 02 (own JVM) |
|
||||
| `src/test/.../MessagesTests` | transcripts 04-07 and 10 |
|
||||
| `src/test/.../ProblemDetailTests` | transcripts 08, 09 |
|
||||
|
||||
## Index of captured output
|
||||
|
||||
| File | Written by | What it shows |
|
||||
|---|---|---|
|
||||
| `01-accept-language.txt` | `LocaleResolutionTests` | which locale each `Accept-Language` header resolves to, and what a missing header gives |
|
||||
| `02-fallback-to-system-locale.txt` | `SystemLocaleFallbackTests` | a French or English request served in Hindi because the JVM default is Hindi |
|
||||
| `03-cookie-locale-resolver.txt` | `LocaleResolutionTests` | `?lang=mr`, the `Set-Cookie`, the cookie honoured later; and the two wiring mistakes |
|
||||
| `04-messageformat-apostrophes.txt` | `MessagesTests` | `''` vs `'`, with and without arguments |
|
||||
| `05-devanagari-digits.txt` | `MessagesTests` | Marathi renders `1234567` in Devanagari digits, Hindi does not; two ways to keep Latin digits |
|
||||
| `06-missing-keys.txt` | `MessagesTests` | a key present only in English, and a key present nowhere |
|
||||
| `07-properties-encoding.txt` | `MessagesTests` | the same file read as UTF-8 and as ISO-8859-1 |
|
||||
| `08-validation-messages.txt` | `ProblemDetailTests` | a 400 in four languages, validation messages and all |
|
||||
| `09-problemdetail-localized.txt` | `ProblemDetailTests` | three kinds of exception, translated or not |
|
||||
| `10-always-use-message-format.txt` | `MessagesTests` | the apostrophe lookups with `spring.messages.always-use-message-format=true` |
|
||||
| `11-spring-web-locale.txt` | `LocaleResolutionTests` | `spring.web.locale` and `spring.web.locale-resolver=fixed` |
|
||||
| `12-configuration-metadata.txt` | `capture-facts.sh` | every `spring.messages.*` and `spring.web.locale*` property, type and default |
|
||||
Reference in New Issue
Block a user