diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/context/MessageSourceAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/context/MessageSourceAutoConfiguration.java index 8e803c3ed16..e6dbf3e647c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/context/MessageSourceAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/context/MessageSourceAutoConfiguration.java @@ -19,6 +19,7 @@ package org.springframework.boot.autoconfigure.context; import java.io.IOException; import java.io.UncheckedIOException; import java.time.Duration; +import java.util.List; import java.util.Properties; import org.springframework.aot.hint.RuntimeHints; @@ -41,6 +42,7 @@ import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.ImportRuntimeHints; import org.springframework.context.support.AbstractApplicationContext; import org.springframework.context.support.ResourceBundleMessageSource; +import org.springframework.core.CollectionFactory; import org.springframework.core.Ordered; import org.springframework.core.io.Resource; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; @@ -85,20 +87,24 @@ public class MessageSourceAutoConfiguration { } messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat()); messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage()); + messageSource.setCommonMessages(loadCommonMessages(properties.getCommonMessages())); + return messageSource; + } - try { - if (properties.getCommonMessages() != null) { - Properties commonProperties = new Properties(); - for (Resource commonResource : properties.getCommonMessages()) { - PropertiesLoaderUtils.fillProperties(commonProperties, commonResource); - } - messageSource.setCommonMessages(commonProperties); + private Properties loadCommonMessages(List resources) { + if (CollectionUtils.isEmpty(resources)) { + return null; + } + Properties properties = CollectionFactory.createSortedProperties(false); + for (Resource resource : resources) { + try { + PropertiesLoaderUtils.fillProperties(properties, resource); + } + catch (IOException ex) { + throw new UncheckedIOException("Failed to load common messages from '%s'".formatted(resource), ex); } } - catch (IOException ex) { - throw new UncheckedIOException("Failed to load common messages", ex); - } - return messageSource; + return properties; } protected static class ResourceBundleCondition extends SpringBootCondition { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/context/MessageSourceProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/context/MessageSourceProperties.java index 508be001a38..7fcf2dbf252 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/context/MessageSourceProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/context/MessageSourceProperties.java @@ -46,7 +46,7 @@ public class MessageSourceProperties { private List basename = new ArrayList<>(List.of("messages")); /** - * Comma-separated list of locale-independent common messages. + * List of locale-independent property file resources containing common messages. */ private List commonMessages; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/context/MessageSourceAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/context/MessageSourceAutoConfigurationTests.java index 523a5109743..180c71a394f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/context/MessageSourceAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/context/MessageSourceAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -58,7 +58,7 @@ class MessageSourceAutoConfigurationTests { @Test void propertiesBundleWithSlashIsDetected() { - this.contextRunner.withPropertyValues("spring.messages.basename:test/messages").run((context) -> { + this.contextRunner.withPropertyValues("spring.messages.basename=test/messages").run((context) -> { assertThat(context).hasSingleBean(MessageSource.class); assertThat(context.getMessage("foo", null, "Foo message", Locale.UK)).isEqualTo("bar"); }); @@ -66,7 +66,7 @@ class MessageSourceAutoConfigurationTests { @Test void propertiesBundleWithDotIsDetected() { - this.contextRunner.withPropertyValues("spring.messages.basename:test.messages").run((context) -> { + this.contextRunner.withPropertyValues("spring.messages.basename=test.messages").run((context) -> { assertThat(context).hasSingleBean(MessageSource.class); assertThat(context.getMessage("foo", null, "Foo message", Locale.UK)).isEqualTo("bar"); }); @@ -74,7 +74,7 @@ class MessageSourceAutoConfigurationTests { @Test void testEncodingWorks() { - this.contextRunner.withPropertyValues("spring.messages.basename:test/swedish") + this.contextRunner.withPropertyValues("spring.messages.basename=test/swedish") .run((context) -> assertThat(context.getMessage("foo", null, "Foo message", Locale.UK)) .isEqualTo("Some text with some swedish öäå!")); } @@ -82,14 +82,14 @@ class MessageSourceAutoConfigurationTests { @Test void testCacheDurationNoUnit() { this.contextRunner - .withPropertyValues("spring.messages.basename:test/messages", "spring.messages.cache-duration=10") + .withPropertyValues("spring.messages.basename=test/messages", "spring.messages.cache-duration=10") .run(assertCache(10 * 1000)); } @Test void testCacheDurationWithUnit() { this.contextRunner - .withPropertyValues("spring.messages.basename:test/messages", "spring.messages.cache-duration=1m") + .withPropertyValues("spring.messages.basename=test/messages", "spring.messages.cache-duration=1m") .run(assertCache(60 * 1000)); } @@ -102,7 +102,7 @@ class MessageSourceAutoConfigurationTests { @Test void testMultipleMessageSourceCreated() { - this.contextRunner.withPropertyValues("spring.messages.basename:test/messages,test/messages2") + this.contextRunner.withPropertyValues("spring.messages.basename=test/messages,test/messages2") .run((context) -> { assertThat(context.getMessage("foo", null, "Foo message", Locale.UK)).isEqualTo("bar"); assertThat(context.getMessage("foo-foo", null, "Foo-Foo message", Locale.UK)).isEqualTo("bar-bar"); @@ -119,14 +119,24 @@ class MessageSourceAutoConfigurationTests { @Test void testCommonMessages() { this.contextRunner - .withPropertyValues("spring.messages.basename:test/messages", - "spring.messages.common-messages:test/common-messages") + .withPropertyValues("spring.messages.basename=test/messages", + "spring.messages.common-messages=classpath:test/common-messages.properties") .run((context) -> assertThat(context.getMessage("hello", null, "Hello!", Locale.UK)).isEqualTo("world")); } + @Test + void testCommonMessagesWhenNotFound() { + this.contextRunner + .withPropertyValues("spring.messages.basename=test/messages", + "spring.messages.common-messages=classpath:test/common-messages-missing.properties") + .run((context) -> assertThat(context).getFailure() + .hasMessageContaining( + "Failed to load common messages from 'class path resource [test/common-messages-missing.properties]'")); + } + @Test void testFallbackDefault() { - this.contextRunner.withPropertyValues("spring.messages.basename:test/messages") + this.contextRunner.withPropertyValues("spring.messages.basename=test/messages") .run((context) -> assertThat(context.getBean(MessageSource.class)) .hasFieldOrPropertyWithValue("fallbackToSystemLocale", true)); } @@ -134,7 +144,7 @@ class MessageSourceAutoConfigurationTests { @Test void testFallbackTurnOff() { this.contextRunner - .withPropertyValues("spring.messages.basename:test/messages", + .withPropertyValues("spring.messages.basename=test/messages", "spring.messages.fallback-to-system-locale:false") .run((context) -> assertThat(context.getBean(MessageSource.class)) .hasFieldOrPropertyWithValue("fallbackToSystemLocale", false)); @@ -142,7 +152,7 @@ class MessageSourceAutoConfigurationTests { @Test void testFormatMessageDefault() { - this.contextRunner.withPropertyValues("spring.messages.basename:test/messages") + this.contextRunner.withPropertyValues("spring.messages.basename=test/messages") .run((context) -> assertThat(context.getBean(MessageSource.class)) .hasFieldOrPropertyWithValue("alwaysUseMessageFormat", false)); } @@ -150,7 +160,7 @@ class MessageSourceAutoConfigurationTests { @Test void testFormatMessageOn() { this.contextRunner - .withPropertyValues("spring.messages.basename:test/messages", + .withPropertyValues("spring.messages.basename=test/messages", "spring.messages.always-use-message-format:true") .run((context) -> assertThat(context.getBean(MessageSource.class)) .hasFieldOrPropertyWithValue("alwaysUseMessageFormat", true)); @@ -158,7 +168,7 @@ class MessageSourceAutoConfigurationTests { @Test void testUseCodeAsDefaultMessageDefault() { - this.contextRunner.withPropertyValues("spring.messages.basename:test/messages") + this.contextRunner.withPropertyValues("spring.messages.basename=test/messages") .run((context) -> assertThat(context.getBean(MessageSource.class)) .hasFieldOrPropertyWithValue("useCodeAsDefaultMessage", false)); } @@ -166,8 +176,8 @@ class MessageSourceAutoConfigurationTests { @Test void testUseCodeAsDefaultMessageOn() { this.contextRunner - .withPropertyValues("spring.messages.basename:test/messages", - "spring.messages.use-code-as-default-message:true") + .withPropertyValues("spring.messages.basename=test/messages", + "spring.messages.use-code-as-default-message=true") .run((context) -> assertThat(context.getBean(MessageSource.class)) .hasFieldOrPropertyWithValue("useCodeAsDefaultMessage", true)); } @@ -181,13 +191,13 @@ class MessageSourceAutoConfigurationTests { @Test void existingMessageSourceInParentIsIgnored() { this.contextRunner.run((parent) -> this.contextRunner.withParent(parent) - .withPropertyValues("spring.messages.basename:test/messages") + .withPropertyValues("spring.messages.basename=test/messages") .run((context) -> assertThat(context.getMessage("foo", null, "Foo message", Locale.UK)).isEqualTo("bar"))); } @Test void messageSourceWithNonStandardBeanNameIsIgnored() { - this.contextRunner.withPropertyValues("spring.messages.basename:test/messages") + this.contextRunner.withPropertyValues("spring.messages.basename=test/messages") .withUserConfiguration(CustomBeanNameMessageSourceConfiguration.class) .run((context) -> assertThat(context.getMessage("foo", null, Locale.US)).isEqualTo("bar")); } diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/internationalization.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/internationalization.adoc index 13557b033f8..3177f0d5fc3 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/internationalization.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/internationalization.adoc @@ -14,10 +14,12 @@ The basename of the resource bundle as well as several other attributes can be c ---- spring: messages: - basename: "messages,config.i18n.messages" + basename: "messages, config.i18n.messages" + common-messages: "classpath:my-common-messages.properties" fallback-to-system-locale: false ---- -TIP: `spring.messages.basename` supports comma-separated list of locations, either a package qualifier or a resource resolved from the classpath root. +TIP: The configprop:spring.messages.basename[] property supports a list of locations, either a package qualifier or a resource resolved from the classpath root. +The configprop:spring.messages.common-messages[] property supports a list of property file resources. See javadoc:org.springframework.boot.autoconfigure.context.MessageSourceProperties[] for more supported options.