Spring Boot Internationalization: ResourceBundle & MessageSource

The complete Spring Boot i18n picture, with every claim executed against Spring Framework 6.2.7 on JDK 17 and the auto-configuration source read at both Boot 3.5 and Boot 4.0 — which differ only in nullability annotations, so every default holds for both. Opens with the boundary that explains everything: Spring owns the formatting and the configuration, the JDK's 1997-vintage ResourceBundle owns which file gets opened. Then the four silent failures. The auto-configuration condition searches only for the un-suffixed messages.properties, so a bundle of nothing but messages_de.properties produces no MessageSource bean and a parentless DelegatingMessageSource that throws on every key. ResourceBundleMessageSource sets ISO-8859-1 in its own constructor, so the moment you declare your own bean you opt back into mojibake — and because the JDK bundle cache is keyed by name, locale and loader with the ResourceBundle.Control excluded, two message sources over one basename share a decoded bundle and the loser's encoding is discarded without a log line. fallback-to-system-locale defaults to true and the JDK ranks the default-locale bundle above your base file, which is why an English request on a German host returns "Hallo" for keys that exist in the German bundle and English for keys that do not — a page in two languages, reproducible only in production. And MessageFormat runs only when arguments are passed, so a French translator writing N'oubliez pas silently deletes both the apostrophe and every placeholder after it, demonstrated end to end. Plus ChoiceFormat getting Russian wrong above 20 against an ICU4J MessageSource that gets 21 and 102 right and makes lone apostrophes safe, what Boot already wires for Bean Validation messages (the LocalValidatorFactoryBean recipe everyone still repeats is obsolete), the LocaleResolver whose setLocale throws by design, an executed prop2po/po2prop round trip with the two flags that decide whether it works, a twenty-line placeholder gate that fails the build on a broken translation, and a thirteen-row symptom table.

Back to the PO-File blog