Spring ResourceBundleMessageSource Example with Spring Boot

Show text in the user’s language with Spring’s ResourceBundleMessageSource and one messages.properties file per language. Learn getMessage() with arguments, the order in which Spring checks the files, UTF-8, missing keys and the Spring Boot setup.

Spring Framework

Spring’s ResourceBundleMessageSource picks the right text for a key and a language from our properties files. We write one properties file per locale (a language, sometimes with a country, such as fr for French or fr_CA for French in Canada) and put the files in src/main/resources.

We use ResourceBundleMessageSource to show an app in the language of each user, for example a recipe site with English and French visitors.

The following example has an English and a French file, registers the MessageSource bean, or lets Spring Boot create it, and calls getMessage() for both languages.

# messages.properties (default, English)
greeting=Hello, {0}!

# messages_fr.properties (French)
greeting=Bonjour, {0} !
@Bean
public MessageSource messageSource() {
  ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
  messageSource.setBasenames("messages");          // messages*.properties
  messageSource.setDefaultEncoding("UTF-8");
  return messageSource;
}
spring.messages.basename=messages
String french = messageSource.getMessage("greeting", new Object[]{"Ana"}, Locale.FRENCH);     // "Bonjour, Ana !"
String english = messageSource.getMessage("greeting", new Object[]{"Ana"}, Locale.ENGLISH);   // "Hello, Ana!"

Notice that the placeholder {0} gets the first value of the Object[] argument in both languages.

Next, we see the order in which Spring checks the files, handle missing keys and accented letters, compare the two main classes, and use the messages in Spring Boot, validation and Thymeleaf.

1. The MessageSource Interface

Supporting several languages is called internationalization, or i18n for short, and it means the application shows the same text in each language. We keep the texts of one language in one properties file, and a group of these files is called a resource bundle. All files of one bundle start with the same name, the basename (such as messages), and differ only in the locale at the end, such as messages_fr.properties.

MessageSource is the Spring interface with the getMessage() methods. The ApplicationContext interface extends MessageSource, so every Spring container can return messages, and it passes the calls to a bean named messageSource. Spring has three classes we can use for that bean.

ImplementationReads messages fromTypical use
ResourceBundleMessageSourceProperties files on the classpath, through the JDK class java.util.ResourceBundleMost applications. Spring Boot’s default
ReloadableResourceBundleMessageSourceAny Spring resource: classpath:, file:, web rootFiles that change while the application runs
StaticMessageSourceMessages added in codeTests

Spring finds the MessageSource by the bean name messageSource, not by its type. If we name the bean differently, context.getMessage() does not use our files.

2. Using ResourceBundleMessageSource

The following example is a small recipe book app built with Spring Boot 4.1.1 (Spring Framework 7) and Java 25. Its resource bundle has three files in src/main/resources, namely the English default file, a French file and a file for French in Canada.

2.1. Create the Resource Bundle

Each file uses the same keys. A value can contain placeholders such as {0}, which getMessage() replaces with the arguments in the given order.

app.title=Recipe Book
greeting=Hello, {0}!
recipe.count=We have {0,number} recipes.
recipe.saved=Recipe ''{0}'' saved.
recipe.tip=Don't forget the salt.
recipe.name.required=Recipe name is required
recipe.name.size=Short name must be at most {max} characters
app.title=Livre de recettes
greeting=Bonjour, {0} !
recipe.count=Nous avons {0,number} recettes.
recipe.name.required=Le nom de la recette est obligatoire
app.title=Livre de recettes (Canada)

A language file does not need every key. For example, recipe.tip exists only in messages.properties, so French readers get the English text for that key.

2.2. Configure ResourceBundleMessageSource

We create the bean in a @Configuration class, where the method name messageSource() becomes the bean name that Spring looks for. Inside the method, we set the basename of the resource bundle.

@Configuration
public class AppConfig {

  @Bean
  public MessageSource messageSource() {
    ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
    messageSource.setBasenames("messages");
    messageSource.setDefaultEncoding("UTF-8");
    messageSource.setFallbackToSystemLocale(false);
    return messageSource;
  }
}

In XML configuration, the same bean is a <bean> element with the id messageSource.

<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
  <property name="basenames" value="messages"/>
  <property name="defaultEncoding" value="UTF-8"/>
  <property name="fallbackToSystemLocale" value="false"/>
</bean>

We use a few setters again and again, and each one changes one behavior of the bean.

SetterDefaultEffect
setBasenames(“messages”, “errors”)noneOne or more bundles. If two bundles have the same key, the first basename wins
setDefaultEncoding(“UTF-8”)ISO-8859-1The character set used to read the files
setFallbackToSystemLocale(false)trueWhether Spring tries the server’s default language before messages.properties
setUseCodeAsDefaultMessage(true)falseReturns the key itself instead of throwing NoSuchMessageException
setAlwaysUseMessageFormat(true)falseAlso formats messages without arguments with MessageFormat
setCacheSeconds(60)-1 (cache forever)How long Spring keeps loaded files in memory

2.3. Set the Default Encoding to UTF-8

An encoding is the rule that turns the bytes of a file into letters. Without setDefaultEncoding(“UTF-8”), ResourceBundleMessageSource reads the files as ISO-8859-1, the old default of Java properties files. Our files are saved as UTF-8, so Spring reads accented letters with the wrong rule, and they come out broken. In the example, French users see two wrong characters in place of the accented e in enregistree until we set the encoding.

// French file saved as UTF-8, value "La recette ''{0}'' est enregistr...e."
String broken = noEncoding.getMessage("recipe.saved", new Object[]{"Crepes"}, Locale.FRENCH);   // "...enregistr\u00c3\u00a9e." (broken: two characters)
String correct = utf8.getMessage("recipe.saved", new Object[]{"Crepes"}, Locale.FRENCH);         // "...enregistr\u00e9e."  (correct)

The escape codes show the exact characters the test compared. Spring Boot sets UTF-8 by default with spring.messages.encoding, so the problem shows up mostly in plain Spring projects and in old ReloadableResourceBundleMessageSource setups.

3. The Order in Which Spring Checks the Files

For a key and a locale, Spring checks the files from the most specific one to the most general one, and stops at the first file that contains the key.

  • First, Spring tries the file for the language and the country. For Locale.CANADA_FRENCH (fr_CA), the file is messages_fr_CA.properties.
  • If that file does not exist or does not contain the key, Spring tries the file for the language only. Here, the file is messages_fr.properties.
  • If fallbackToSystemLocale is true, Spring then tries the files for the server’s default language.
  • If no other file contains the key, Spring uses the default messages.properties.
Two lookup chains. For fr_CA, the key greeting is missing in messages_fr_CA.properties and found in messages_fr.properties. For a German user with a French JVM, messages_de.properties is missing. With fallbackToSystemLocale true, Spring uses messages_fr.properties. With false, Spring uses messages.properties
A German user on a server with French as the default language. fallbackToSystemLocale decides between the French text and the default file.

The lookups in the example follow the same chain as the diagram.

String title = messageSource.getMessage("app.title", null, Locale.CANADA_FRENCH);                  // "Livre de recettes (Canada)"
String greeting = messageSource.getMessage("greeting", new Object[]{"Ana"}, Locale.CANADA_FRENCH);  // "Bonjour, Ana !"  (from messages_fr)
String tip = messageSource.getMessage("recipe.tip", null, Locale.FRENCH);                          // "Don't forget the salt."  (from messages)
String german = messageSource.getMessage("greeting", new Object[]{"Ana"}, Locale.GERMAN);           // "Hello, Ana!"  (no messages_de)

3.1. Falling Back to the System Locale

The system locale is the default language of the JVM, returned by Locale.getDefault(), which the JVM takes from the operating system. Suppose a German user calls a server whose default language is French, and there is no German file. By default, fallbackToSystemLocale is true, so the German user gets French text.

// fallbackToSystemLocale = true (default)
String withFallback = messageSource.getMessage("greeting", new Object[]{"Ana"}, Locale.GERMAN);      // "Bonjour, Ana !"

// fallbackToSystemLocale = false
String withoutFallback = messageSource.getMessage("greeting", new Object[]{"Ana"}, Locale.GERMAN);   // "Hello, Ana!"

So the same application can answer differently on a developer laptop and on a production server. To avoid the difference, we call setFallbackToSystemLocale(false), or in Spring Boot, we set spring.messages.fallback-to-system-locale=false, so messages.properties is always the last file Spring tries.

4. Fetching Messages With getMessage()

MessageSource has three getMessage() methods that find the text the same way and differ only when no file contains the key.

MethodKey missing
getMessage(code, args, locale)Throws NoSuchMessageException
getMessage(code, args, defaultMessage, locale)Returns defaultMessage
getMessage(resolvable, locale)Tries each code of a MessageSourceResolvable, then its default message

4.1. Message Arguments and Formats

We pass the arguments as an Object[], in the order of the placeholders, and Spring fills them in with the JDK class java.text.MessageFormat. A placeholder can also name a format, so {0,number} writes 1250 as “1,250” in English.

String greeting = messageSource.getMessage("greeting", new Object[]{"Ana"}, Locale.FRENCH);     // "Bonjour, Ana !"
String count = messageSource.getMessage("recipe.count", new Object[]{1250}, Locale.ENGLISH);    // "We have 1,250 recipes."
String title = messageSource.getMessage("app.title", null, Locale.FRENCH);                      // "Livre de recettes"

When the message has no placeholders, we pass null as the arguments. Besides number, MessageFormat supports the date, time and choice formats.

4.2. Single Quotes in Messages

MessageFormat reads a single quote as the start of quoted text and drops it, so a message with arguments needs two single quotes for one apostrophe. By default, Spring does not pass messages without arguments to MessageFormat, so one quote works in those messages.

// wrong=  {0} can't be empty
// right=  {0} can''t be empty
// tip=    Don't forget the salt.
String wrong = source.getMessage("wrong", new Object[]{"Name"}, Locale.ENGLISH);   // "Name cant be empty"
String right = source.getMessage("right", new Object[]{"Name"}, Locale.ENGLISH);   // "Name can't be empty"
String tip = source.getMessage("tip", null, Locale.ENGLISH);                       // "Don't forget the salt."
// with setAlwaysUseMessageFormat(true)
String formattedTip = source.getMessage("tip", null, Locale.ENGLISH);              // "Dont forget the salt."

For the same reason, the recipe.saved message of the example uses ''{0}''. The result is Recipe ‘Pancakes’ saved.

4.3. Default Message and NoSuchMessageException

A missing key throws NoSuchMessageException unless we pass a default message. The exception message names the key and the locale, so the log shows which translation is missing.

String deleted = messageSource.getMessage("recipe.deleted", null, "Recipe deleted", Locale.ENGLISH);
// "Recipe deleted"

String missing = messageSource.getMessage("recipe.deleted", null, Locale.FRENCH);
// NoSuchMessageException: No message found under code 'recipe.deleted' for locale 'fr'.

With setUseCodeAsDefaultMessage(true), the same call returns the key “recipe.deleted” and does not throw. This helps during development, but in production it hides missing translations, because the page shows the key and Spring logs no error.

4.4. Using the ApplicationContext Directly

Because ApplicationContext extends MessageSource, we can call getMessage() on the context itself, which passes the call to the messageSource bean.

try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
  String title = context.getMessage("app.title", null, Locale.FRENCH);   // "Livre de recettes"
}

5. ResourceBundleMessageSource vs ReloadableResourceBundleMessageSource

ReloadableResourceBundleMessageSource loads the files in a different way. The plain class uses the JDK’s java.util.ResourceBundle, whereas the reloadable class uses Spring’s Resource classes, which can read a file from many places. So the reloadable class can read files outside the JAR and pick up changed files without a restart.

ResourceBundleMessageSourceReloadableResourceBundleMessageSource
Basenamemessages (classpath only)classpath:messages, file:/opt/app/i18n/messages, WEB-INF/messages
Loadingjava.util.ResourceBundleSpring Resource and Properties
Reload changed filesOnly when the cache expires. The javadoc points to the reloadable class for reloadingYes, after setCacheSeconds(n). Spring checks the file’s last-changed time
XML properties filesNoYes (messages.xml)
Encoding per fileNo, one defaultEncodingYes, setFileEncodings()
Default encodingISO-8859-1ISO-8859-1
Spring Boot auto-configurationYesNo, we declare the bean ourselves
@Bean
public MessageSource messageSource() {
  ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
  messageSource.setBasenames("classpath:messages", "file:/opt/recipes/i18n/messages");
  messageSource.setDefaultEncoding("UTF-8");
  messageSource.setCacheSeconds(10);   // check the files every 10 seconds
  return messageSource;
}

For files in src/main/resources, the basename of ReloadableResourceBundleMessageSource needs the classpath: prefix, because without a prefix, a web application looks for the files in the web root folder. Reloading is meant for files on the file system, and the Javadoc warns that for classpath: files with a cacheSeconds value other than -1, reloading may not work reliably.

6. Using MessageSourceAware and MessageSourceAccessor

To use messages in our own beans, we inject the MessageSource bean through the constructor. The older way still works. A bean implements the MessageSourceAware interface, and Spring calls its setMessageSource() method for us.

@Service
public class RecipeService implements MessageSourceAware {

  private MessageSource messageSource;

  @Override
  public void setMessageSource(MessageSource messageSource) {
    this.messageSource = messageSource;
  }
}

MessageSourceAccessor is a small helper class that wraps a MessageSource, so its getMessage() methods do not need a locale argument. The accessor uses its own default locale, and if we did not give one, it asks LocaleContextHolder.getLocale(). LocaleContextHolder stores the locale of the current thread, which in Spring MVC is the language of the current HTTP request.

MessageSourceAccessor accessor = new MessageSourceAccessor(messageSource, Locale.FRENCH);
String greeting = accessor.getMessage("greeting", new Object[]{"Ana"});    // "Bonjour, Ana !"
String title = accessor.getMessage("app.title", Locale.ENGLISH);           // "Recipe Book"

In the example, the RecipeMessages service creates the accessor without a default locale, so every call uses the locale from LocaleContextHolder.

@Service
public class RecipeMessages {

  private final MessageSourceAccessor messages;

  public RecipeMessages(MessageSource messageSource) {
    this.messages = new MessageSourceAccessor(messageSource);
  }

  public String greet(String name) {
    return messages.getMessage("greeting", new Object[]{name});
  }
}
LocaleContextHolder.setLocale(Locale.FRENCH);
String greeting = recipeMessages.greet("Ana");      // "Bonjour, Ana !"

7. Messages in Spring Boot

Spring Boot creates the messageSource bean for us when messages.properties is on the classpath. The bean is a ResourceBundleMessageSource, and we configure it with the *spring.messages.** properties.

spring.messages.basename=messages
spring.messages.encoding=UTF-8
spring.messages.fallback-to-system-locale=false

Each property matches one setter from section 2.2.

PropertyDefaultSame as
spring.messages.basenamemessagessetBasenames(). A comma-separated list. Folders are allowed (i18n/messages)
spring.messages.encodingUTF-8setDefaultEncoding()
spring.messages.fallback-to-system-localetruesetFallbackToSystemLocale()
spring.messages.use-code-as-default-messagefalsesetUseCodeAsDefaultMessage()
spring.messages.always-use-message-formatfalsesetAlwaysUseMessageFormat()
spring.messages.cache-durationnot set (cache forever)setCacheMillis(), for example 30s
spring.messages.common-messagesnot setExtra properties files with messages for all languages

We inject the bean like any other bean, and the test of the example checks its class and the UTF-8 default.

@Autowired
MessageSource messageSource;

Class<?> type = messageSource.getClass();                                             // ResourceBundleMessageSource
String greeting = messageSource.getMessage("greeting", new Object[]{"Ana"}, Locale.FRENCH);   // "Bonjour, Ana !"

If we declare our own bean named messageSource, Spring Boot skips its own internationalization auto-configuration, and the *spring.messages.** properties have no effect.

8. Validation Messages From messages.properties

Bean Validation annotations such as @NotBlank accept a message key in braces. Spring Boot’s validator looks up these keys in the MessageSource first, so the validation messages are in the same files as our other messages, and then it fills in the annotation values, such as {max}.

public record Recipe(
    @NotBlank(message = "{recipe.name.required}") String name,
    @Size(max = 10, message = "{recipe.name.size}") String shortName) {
}
LocaleContextHolder.setLocale(Locale.ENGLISH);
Set<ConstraintViolation<Recipe>> english = validator.validate(new Recipe("", "Pancakes with syrup"));
// "Recipe name is required", "Short name must be at most 10 characters"

LocaleContextHolder.setLocale(Locale.FRENCH);
Set<ConstraintViolation<Recipe>> french = validator.validate(new Recipe("", "ok"));
// "Le nom de la recette est obligatoire"

The validator takes the locale from LocaleContextHolder, which Spring MVC sets for each request. The same messages appear in a REST response when request validation fails.

9. Messages in Spring MVC and Thymeleaf

In a web application, a LocaleResolver picks the language of each request, and by default, Spring Boot reads the browser’s Accept-Language header. To let users switch languages, we can store the language in a cookie and read a ?lang=fr parameter, and a REST API returns translated messages from the same files.

Thymeleaf templates read the same MessageSource with the #{…} syntax.

<h1 th:text="#{app.title}">Recipe Book</h1>
<p th:text="#{greeting('Ana')}">Hello, Ana!</p>

A complete Spring Boot app with Thymeleaf pages uses the same syntax in every template.

10. MessageSource FAQs

10.1. Why Does Spring Boot Not Find My messages.properties?

Spring Boot creates the messageSource bean only when a file with the configured basename is on the classpath. Otherwise, the context registers an empty DelegatingMessageSource, so every getMessage() call without a default message throws NoSuchMessageException. Three mistakes lead to the empty DelegatingMessageSource.

  • The file is in a subfolder such as src/main/resources/i18n, but spring.messages.basename is still messages instead of i18n/messages.
  • The basename includes the extension, as in messages.properties, but a basename never has the extension.
  • Our own @Bean method has another name, such as myMessages, so Spring Boot still creates its own messageSource bean, and context.getMessage() and validation use that bean, not ours.

10.2. How Do I Use Several Message Files?

We list several basenames, and Spring checks them in the given order, so if two bundles have the same key, the first bundle wins.

spring.messages.basename=messages,errors,i18n/labels

In plain Spring, the same list goes to setBasenames(“messages”, “errors”, “i18n/labels”).

10.3. Can I Use java.util.ResourceBundle Without Spring?

Yes. ResourceBundle is the JDK class that ResourceBundleMessageSource uses inside, so without Spring, we load the bundle and fill in the arguments ourselves.

ResourceBundle bundle = ResourceBundle.getBundle("messages", Locale.of("fr"));
String pattern = bundle.getString("greeting");     // "Bonjour, {0} !"
String message = MessageFormat.format(pattern, "Ana");   // "Bonjour, Ana !"

Since Java 19, Locale.of() replaces the deprecated Locale constructors, and the other ways to create a Java Locale still work. On top of the JDK class, Spring adds caching, the fallback rules and default messages, and connects the messages to validation and to views.

11. Conclusion

For messages in several languages, most Spring applications need a ResourceBundleMessageSource bean named messageSource and one messages.properties file per language, and they call getMessage(key, args, locale). In plain Spring, we set the encoding to UTF-8 and turn off the system locale fallback, whereas in Spring Boot, the *spring.messages.** properties are enough. We pick ReloadableResourceBundleMessageSource when the files are outside the JAR or change while the application runs.

12. References

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.