Files
spring-boot-demo/i18n/README.md
T
Claude 2cbf31e7d5 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
2026-09-24 09:54:55 +00:00

86 lines
4.9 KiB
Markdown

# 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 |