skip to content

How do you make Java resource bundles (i18n messages) work in a GraalVM native image with Spring, and how does that differ from registering a plain classpath resource?

level: middleimportance: should knowfreq 40%

answer

  1. Base name + locale = bundle family
  2. registerResourceBundle("messages"), not registerPattern
  3. resource-config.json 'bundles' array
  4. ResourceBundleMessageSource vs Reloadable... (which loader)
  5. MissingResourceException only in native

basics

~20 s

Resource bundles (message .properties per locale) need their own registration, not just a file pattern, because ResourceBundle resolves by base name plus locale. Use ResourceHints.registerResourceBundle(baseName) or GraalVM's resource-config.json 'bundles' section, so all locale variants are embedded.

solid answer

~40 s

A `ResourceBundle` (backing Spring's `MessageSource` / i18n) is resolved by a **base name** plus a `Locale`, which the loader expands to files like `messages_en.properties`, `messages_fr.properties`. The native-image builder can't infer which locales you'll request, so bundles need a dedicated registration distinct from a plain resource pattern. In GraalVM config that's the `"bundles"` array in `resource-config.json` (`{"name":"messages"}`); in Spring you call `hints.resources().registerResourceBundle("messages")` inside a `RuntimeHintsRegistrar`. This registers the bundle family so all locale variants are included and `ResourceBundle.getBundle` works at runtime. It differs from `registerPattern` because bundles carry locale-fallback semantics — the base-name registration pulls in the variants, whereas a raw pattern would just embed literal files without the bundle machinery being aware.

code

java · 10 lines
java
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;

class I18nHints implements RuntimeHintsRegistrar {
    @Override
    public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
        // Embeds messages.properties, messages_en.properties, messages_de.properties, ...
        hints.resources().registerResourceBundle("messages");
    }
}

go deeper

for a junior

Knows i18n messages need registration too.

for a middle

Uses registerResourceBundle for bundle base names and knows it differs from registerPattern.

for a senior

Distinguishes ResourceBundleMessageSource (java.util.ResourceBundle) from ReloadableResourceBundleMessageSource (Resource abstraction) and registers each correctly.

for a principal

Controls locale footprint, understands fallback semantics, uses tracing agent to enumerate real locales.

## Resource bundles vs plain resources A **resource bundle** is Java's i18n mechanism: you have a **base name** (e.g. `messages`) and per-locale property files — `messages.properties` (default), `messages_en.properties`, `messages_de_DE.properties`, etc. `ResourceBundle.getBundle("messages", locale)` performs a **fallback search** (specific locale → language → default). In Spring, `ResourceBundleMessageSource` uses exactly this; `MessageSource#getMessage` resolves keys through it. Because bundle lookup is driven by a base name plus a runtime `Locale`, the concrete files accessed depend on runtime data. Under the closed-world build the analysis can't know which locales you'll hit, so bundles are treated as a **first-class registration category**, separate from ordinary resource files. ## Registering bundles **GraalVM `resource-config.json`** has a dedicated `bundles` array: ```json { "bundles": [ { "name": "messages" } ] } ``` Optionally with `"locales": ["en", "de"]` to restrict variants. **Spring RuntimeHints** exposes it via `ResourceHints#registerResourceBundle`: ```java hints.resources().registerResourceBundle("messages"); ``` This is separate from `registerPattern` — it tells the builder 'this is a bundle base name; include its locale family.' ## Why not just registerPattern("messages*.properties")? You sometimes *can* embed the files with a pattern, but that only copies bytes; the **bundle-loading path** in GraalVM has its own handling (and, historically, its own config category) so that `ResourceBundle.getBundle` and control/fallback logic resolve correctly. Using `registerResourceBundle` is the correct, intent-revealing call and avoids subtle locale-fallback gaps. Prefer it for anything loaded via `ResourceBundle`/`ResourceBundleMessageSource`. ## Spring specifics - `ReloadableResourceBundleMessageSource` reads `.properties` via Spring's resource abstraction (not `java.util.ResourceBundle`), so those may register as plain resource patterns instead. `ResourceBundleMessageSource` uses `java.util.ResourceBundle` and wants bundle registration. Know which one your app uses. - Locale set: if you only support a few locales, list them to keep the image lean; otherwise all discoverable variants are embedded. ## Gotchas - Forgetting the bundle registration → `MissingResourceException` only in the native binary. - Encoding: property bundles historically default to ISO-8859-1 unless configured; unrelated to native but a common i18n trap. - The tracing agent captures accessed bundles/locales automatically, a good way to enumerate exactly which locales to register. ## When to use Register bundles whenever i18n messages come through `ResourceBundleMessageSource` or direct `ResourceBundle.getBundle`. Use plain resource patterns for `ReloadableResourceBundleMessageSource` or non-i18n `.properties`.

  • Your app uses ReloadableResourceBundleMessageSource instead of ResourceBundleMessageSource — does bundle registration still apply?
    Not the same way. ReloadableResourceBundleMessageSource reads .properties through Spring's Resource abstraction rather than java.util.ResourceBundle, so you register those files with resource patterns (registerPattern) instead of registerResourceBundle.
  • How can you avoid embedding dozens of locales you don't use?
    Restrict locales explicitly — in resource-config.json use the bundle's 'locales' list, or register only the supported base name and rely on the tracing agent to reveal which locales are actually requested, keeping the image small.

context