What are the key production pitfalls with MessageSource around MessageFormat quoting, encoding, missing keys, and the parent hierarchy?
answer
- '' only matters when args present (no-args skips MessageFormat)
- alwaysUseMessageFormat=true → uniform quoting
- defaultEncoding=UTF-8 (old default ISO-8859-1)
- useCodeAsDefaultMessage hides gaps in prod
- fallbackToSystemLocale=false; setParentMessageSource for shared bundle
basics
~20 sWatch four things: single quotes must be doubled ('') only when a message has arguments; set defaultEncoding to UTF-8; decide missing-key behavior (throw vs default vs useCodeAsDefaultMessage); and set fallbackToSystemLocale=false so missing files fall back to the base bundle, not the server's locale.
solid answer
~40 sSeveral MessageSource behaviors bite in production. (1) MessageFormat quoting: a single quote is an escape char, so 'literal' text disappears and you must double it (''), but only when the message actually has arguments — with no args Spring skips MessageFormat unless alwaysUseMessageFormat=true, so the same file can behave inconsistently. (2) Encoding: default was ISO-8859-1 for ResourceBundleMessageSource; always set defaultEncoding=UTF-8 (Boot does). (3) Missing keys: getMessage without a default throws NoSuchMessageException; useCodeAsDefaultMessage=true returns the code instead — good for dev, risky in prod because it hides gaps. (4) fallbackToSystemLocale=true silently serves the server's default locale before the base file. (5) HierarchicalMessageSource: setParentMessageSource lets a child resolve unknown codes from a parent, and a child ApplicationContext consults its parent's messageSource. Also mind caching (cacheSeconds) and thread-bound LocaleContextHolder in async code.
code
java · 17 lines@Bean
public MessageSource messageSource() {
var common = new ResourceBundleMessageSource();
common.setBasename("i18n/common");
common.setDefaultEncoding("UTF-8");
var app = new ResourceBundleMessageSource();
app.setBasename("i18n/messages");
app.setDefaultEncoding("UTF-8");
app.setFallbackToSystemLocale(false); // fall back to base file, not JVM locale
app.setAlwaysUseMessageFormat(true); // consistent '' quoting
// app.setUseCodeAsDefaultMessage(true); // dev only — hides missing keys in prod
app.setParentMessageSource(common); // unknown codes resolve from the shared bundle
return app;
}
// messages.properties (args present -> double the apostrophe):
// greeting=Bonjour, {0} ! | cannot=We can''t process {0}.go deeper
Aware messages need UTF-8 and placeholders.
Knows about missing-key exceptions and the encoding default.
Explains MessageFormat quoting nuance, fallbackToSystemLocale, and reload caching.
Codifies i18n conventions (encoding, fallback, missing-key policy, parent bundles, thread-local propagation) across services and CI checks.
At scale, `MessageSource` has several sharp edges worth codifying as team conventions. ## Formatting and encoding **1. MessageFormat single-quote escaping.** When arguments are present, Spring runs the text through `java.text.MessageFormat`, where `'` is the escape character. So `Can't {0}` renders as `Cant {0}`'s escaped form — the apostrophe swallows following text. You must **double** it: `Can''t {0}`. - The subtle part: with **no arguments**, `AbstractMessageSource` skips `MessageFormat` entirely (returns the raw string), so `Can't proceed` renders fine — until someone adds an argument to that message and the apostrophe silently breaks. - Setting **`alwaysUseMessageFormat=true`** forces MessageFormat even without args, making behavior consistent (at the cost of requiring `''` everywhere). **2. Encoding.** `ResourceBundleMessageSource` historically defaulted to **ISO-8859-1** (the JDK `ResourceBundle` default before Java 9), mangling accented/non-Latin text. Always set **`defaultEncoding="UTF-8"`**. Spring Boot's `MessageSourceProperties` defaults encoding to UTF-8 for you; hand-rolled beans must set it explicitly. ## Resolution and fallback **3. Missing-key policy.** The no-default overload throws `NoSuchMessageException`. Options: - pass a `defaultMessage`; - or set **`useCodeAsDefaultMessage=true`** so a missing code returns the code string itself. The latter is convenient in dev but **dangerous in prod** — it masks untranslated keys as leaked technical codes in the UI. Prefer failing tests or a lint step that verifies every code exists in every locale file. **4. Locale fallback trap.** `fallbackToSystemLocale` defaults to **true**: for a missing locale file Spring tries the **JVM default locale** before the base bundle. A server started in `fr_FR` can serve French to an English visitor whose file is absent. Set it **false** so fallback goes straight to `messages.properties`. (Spring 5.2.2+ also allows a `defaultLocale`.) **5. Parent MessageSource hierarchy.** Both concrete sources implement **`HierarchicalMessageSource`** (`setParentMessageSource`). Resolution first tries the child, then delegates unknown codes to the parent — letting you layer a shared/common bundle under module-specific ones. Additionally, a child `ApplicationContext` automatically consults its **parent context's** `messageSource`, so codes defined once in a parent context are visible to children. ## Runtime behavior **6. Caching & reload.** `cacheSeconds`/`cacheMillis` control reload frequency (ReloadableResourceBundleMessageSource). - `-1` = cache forever; - `0` = re-read every access (dev only, expensive). Tune for prod (e.g. 300s) if you externalize files. **7. Thread-bound locale.** `LocaleContextHolder` is a thread-local; async tasks, `@Async`, reactive schedulers, and thread pools don't inherit it unless you propagate it. Pass an explicit `Locale` into background work rather than relying on the holder. ## Convention takeaways - UTF-8 always; - `fallbackToSystemLocale=false`; - a deliberate missing-key policy backed by tests; - `alwaysUseMessageFormat=true` if you want quoting to be uniform; - a parent common bundle for shared strings.
- Why can the same apostrophe render correctly in one message but break in another?MessageFormat is only applied when a message has arguments (unless alwaysUseMessageFormat=true). A no-arg message returns raw text so a single ' is fine; add an argument and MessageFormat treats ' as an escape, so you must double it to ''.
- Why avoid useCodeAsDefaultMessage=true in production?It returns the raw code when a key is missing, so untranslated messages surface as technical strings (e.g. 'NotNull.user.email') in the UI, silently hiding translation gaps. Prefer explicit defaults plus tests that assert every code exists per locale.
saying these in an interview costs you the question
- Doubling apostrophes everywhere without knowing it only matters with args (or alwaysUseMessageFormat)
- Leaving defaultEncoding unset and getting mojibake
- Enabling useCodeAsDefaultMessage in prod and leaking codes to users
- Assuming LocaleContextHolder propagates into @Async/reactive threads