diff --git a/.github/native-tests.json b/.github/native-tests.json index f7cdd076826dc6..631bc2fa21730f 100644 --- a/.github/native-tests.json +++ b/.github/native-tests.json @@ -105,7 +105,7 @@ { "category": "Misc2", "timeout": 75, - "test-modules": "hibernate-validator, test-extension/tests, logging-gelf, mailer, native-config-profile, locales/all, locales/some", + "test-modules": "hibernate-validator, test-extension/tests, logging-gelf, mailer, native-config-profile, locales/all, locales/some, locales/default", "os-name": "ubuntu-latest" }, { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/NativeConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/NativeConfig.java index 12c4728038d71a..d3fc8972f635e8 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/NativeConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/NativeConfig.java @@ -88,7 +88,8 @@ public interface NativeConfig { /** * Defines the user language used for building the native executable. - * It also serves as the default Locale language for the native executable application runtime. + * With GraalVM versions prior to GraalVM for JDK 24 it also serves as the default Locale language for the native executable + * application runtime. * e.g. en or cs as defined by IETF BCP 47 language tags. *

* @@ -100,7 +101,8 @@ public interface NativeConfig { /** * Defines the user country used for building the native executable. - * It also serves as the default Locale country for the native executable application runtime. + * With GraalVM versions prior to GraalVM for JDK 24 it also serves as the default Locale country for the native executable + * application runtime. * e.g. US or FR as defined by ISO 3166-1 alpha-2 codes. *

* diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/GraalVM.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/GraalVM.java index f65a2f9355de53..130991d72255c9 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/GraalVM.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/GraalVM.java @@ -195,6 +195,8 @@ public static final class Version implements Comparable { public static final Version VERSION_23_1_3 = new Version("GraalVM 23.1.3", "23.1.3", "21", Distribution.GRAALVM); public static final Version VERSION_24_0_0 = new Version("GraalVM 24.0.0", "24.0.0", "22", Distribution.GRAALVM); public static final Version VERSION_24_0_999 = new Version("GraalVM 24.0.999", "24.0.999", "22", Distribution.GRAALVM); + public static final Version VERSION_24_1_0 = new Version("GraalVM 24.1.0", "24.1.0", "23", Distribution.GRAALVM); + public static final Version VERSION_24_2_0 = new Version("GraalVM 24.2.0", "24.2.0", "24", Distribution.GRAALVM); /** * The minimum version of GraalVM supported by Quarkus. diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java index c1f7f35c942525..7dd1ecdb2eaa87 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java @@ -747,6 +747,14 @@ public NativeImageInvokerInfo build() { } } + if (!Locale.getDefault().equals(localesBuildTimeConfig.defaultLocale) + && graalVMVersion.compareTo(GraalVM.Version.VERSION_24_2_0) >= 0) { + log.warn( + "Your application is setting the 'quarkus.default-locale' configuration key. " + + "Starting with GraalVM/Mandrel for JDK 24 this configuration is being ignored and the " + + "default locale is always set at runtime based on the system default locale."); + } + final String userLanguage = LocaleProcessor.nativeImageUserLanguage(nativeConfig, localesBuildTimeConfig); if (!userLanguage.isEmpty()) { nativeImageArgs.add("-J-Duser.language=" + userLanguage); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/LocaleProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/LocaleProcessor.java index 8db1bafb7a1096..51d64eb577199a 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/LocaleProcessor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/LocaleProcessor.java @@ -139,13 +139,15 @@ public static String nativeImageIncludeLocales(NativeConfig nativeConfig, Locale return "all"; } - // We subtract what we already declare for native-image's user.language or user.country. - // Note the deprecated options still count. - additionalLocales.remove(localesBuildTimeConfig.defaultLocale); + // GraalVM for JDK 24 doesn't include the default locale used at build time. We must explicitly include the + // specified locales - including the build-time locale. + // Note the deprecated options still count and take precedence. if (nativeConfig.userCountry().isPresent() && nativeConfig.userLanguage().isPresent()) { - additionalLocales.remove(new Locale(nativeConfig.userLanguage().get(), nativeConfig.userCountry().get())); + additionalLocales.add(new Locale(nativeConfig.userLanguage().get(), nativeConfig.userCountry().get())); } else if (nativeConfig.userLanguage().isPresent()) { - additionalLocales.remove(new Locale(nativeConfig.userLanguage().get())); + additionalLocales.add(new Locale(nativeConfig.userLanguage().get())); + } else { + additionalLocales.add(localesBuildTimeConfig.defaultLocale); } return additionalLocales.stream() diff --git a/core/runtime/src/main/java/io/quarkus/runtime/LocalesBuildTimeConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/LocalesBuildTimeConfig.java index 7ca78d84ceeb0b..cf80f890c8db66 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/LocalesBuildTimeConfig.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/LocalesBuildTimeConfig.java @@ -44,8 +44,11 @@ public class LocalesBuildTimeConfig { * For instance, the Hibernate Validator extension makes use of it. *

* Native-image build uses this property to derive {@code user.language} and {@code user.country} for the application's - * runtime. + * runtime. Starting with GraalVM for JDK 24 this option will not result in setting the default runtime locale. + * Instead, the Java configuration {@code user.language} and {@code user.country} will be used as the + * default when running the native image. */ - @ConfigItem(defaultValue = DEFAULT_LANGUAGE + "-" + DEFAULT_COUNTRY, defaultValueDocumentation = "Build system locale") + @ConfigItem(defaultValue = DEFAULT_LANGUAGE + "-" + + DEFAULT_COUNTRY, defaultValueDocumentation = "Build system locale prior to GraalVM for JDK 24, or system default locale at image runtime with GraalVM for JDK 24 and better") public Locale defaultLocale; } diff --git a/integration-tests/locales/default/pom.xml b/integration-tests/locales/default/pom.xml new file mode 100644 index 00000000000000..2d635a1715b6ba --- /dev/null +++ b/integration-tests/locales/default/pom.xml @@ -0,0 +1,97 @@ + + + 4.0.0 + + io.quarkus + quarkus-integration-test-locales-parent + 999-SNAPSHOT + + quarkus-integration-test-locales-default + Quarkus - Integration Tests - Locales - Default + + + io.quarkus + quarkus-rest + + + io.quarkus + quarkus-hibernate-validator + + + io.quarkus + quarkus-integration-test-locales-app + ${project.version} + + + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + + + io.quarkus + quarkus-rest-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-hibernate-validator-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + src/test/resources + true + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + maven-surefire-plugin + + 1 + false + + + + + diff --git a/integration-tests/locales/default/src/main/java/io/quarkus/locales/it/DefaultLocaleResource.java b/integration-tests/locales/default/src/main/java/io/quarkus/locales/it/DefaultLocaleResource.java new file mode 100644 index 00000000000000..3e01732b91e521 --- /dev/null +++ b/integration-tests/locales/default/src/main/java/io/quarkus/locales/it/DefaultLocaleResource.java @@ -0,0 +1,26 @@ +package io.quarkus.locales.it; + +import jakarta.validation.constraints.Pattern; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import org.jboss.logging.Logger; + +@Path("") +public class DefaultLocaleResource extends LocalesResource { + private static final Logger LOG = Logger.getLogger(DefaultLocaleResource.class); + + // @Pattern validation does nothing when placed in LocalesResource. + @GET + @Path("/hibernate-validator-test-validation-message-locale/{id}/") + @Produces(MediaType.TEXT_PLAIN) + public Response validationMessageLocale( + @Pattern(regexp = "A.*", message = "{pattern.message}") @PathParam("id") String id) { + LOG.infof("Triggering test: id: %s", id); + return Response.ok(id).build(); + } +} diff --git a/integration-tests/locales/default/src/test/java/io/quarkus/locales/it/LocalesIT.java b/integration-tests/locales/default/src/test/java/io/quarkus/locales/it/LocalesIT.java new file mode 100644 index 00000000000000..5f71f6d4e1c2c0 --- /dev/null +++ b/integration-tests/locales/default/src/test/java/io/quarkus/locales/it/LocalesIT.java @@ -0,0 +1,84 @@ +package io.quarkus.locales.it; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; + +import org.apache.http.HttpStatus; +import org.jboss.logging.Logger; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import io.quarkus.test.junit.DisableIfBuiltWithGraalVMNewerThan; +import io.quarkus.test.junit.DisableIfBuiltWithGraalVMOlderThan; +import io.quarkus.test.junit.GraalVMVersion; +import io.quarkus.test.junit.QuarkusIntegrationTest; +import io.restassured.RestAssured; + +/** + * For the Native test cases to function, the operating system has to have locales support installed. A barebone system with + * only C.UTF-8 default locale available won't be able to pass the tests. + *

+ * For example, this package satisfies the dependency on a RHEL 9 type of OS: glibc-all-langpacks + */ +@QuarkusIntegrationTest +public class LocalesIT { + + private static final Logger LOG = Logger.getLogger(LocalesIT.class); + + @Test + @DisableIfBuiltWithGraalVMNewerThan(value = GraalVMVersion.GRAALVM_24_1_0) + public void testDefaultLocaleBefore24_2() { + RestAssured.given().when() + .get("/default/de-CH") + .then() + .statusCode(HttpStatus.SC_OK) + /* + * "l-Iżvizzera" is the correct name for Switzerland in Maltese language. + * Maltese is the default language as per quarkus.default-locale=mt-MT. + */ + .body(is("l-Iżvizzera")) + .log().all(); + } + + @Test + @DisableIfBuiltWithGraalVMOlderThan(value = GraalVMVersion.GRAALVM_24_2_0) + public void testDefaultLocaleAfter24_1() { + RestAssured.given().when() + .get("/default/de-CH") + .then() + .statusCode(HttpStatus.SC_OK) + /* + * "l-Iżvizzera" is the correct name for Switzerland in Maltese language. + * Maltese is the default build-time language as per quarkus.default-locale=mt-MT, but not at run-time. + * Note that this test will fail if the default run-time language is Maltese on the test machine, + * this is unfortunate but also unlikely given the small population of Malta. + */ + .body(not("l-Iżvizzera")) + .log().all(); + } + + /** + * @see integration-tests/hibernate-validator/src/test/java/io/quarkus/it/hibernate/validator/HibernateValidatorFunctionalityTest.java + */ + @ParameterizedTest + @CsvSource(value = { + // French locale is included, so it's used, because Croatian locale is not included + // and thus its property file ValidationMessages_hr_HR.properties is ignored. + "en-US;q=0.25,hr-HR;q=0.9,fr-FR;q=0.5,uk-UA;q=0.1|La valeur ne correspond pas à l'échantillon", + // Silent fallback to lingua franca. + "invalid string|Value is not in line with the pattern", + // French locale is available and included. + "en-US;q=0.25,hr-HR;q=1,fr-FR;q=0.5|La valeur ne correspond pas à l'échantillon" + }, delimiter = '|') + public void testValidationMessageLocale(String acceptLanguage, String expectedMessage) { + RestAssured.given() + .header("Accept-Language", acceptLanguage) + .when() + .get("/hibernate-validator-test-validation-message-locale/1") + .then() + .body(containsString(expectedMessage)); + } + +} diff --git a/integration-tests/locales/default/src/test/java/io/quarkus/locales/it/LocalesTest.java b/integration-tests/locales/default/src/test/java/io/quarkus/locales/it/LocalesTest.java new file mode 100644 index 00000000000000..bd42cd4ae7fe71 --- /dev/null +++ b/integration-tests/locales/default/src/test/java/io/quarkus/locales/it/LocalesTest.java @@ -0,0 +1,7 @@ +package io.quarkus.locales.it; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class LocalesTest { +} diff --git a/integration-tests/locales/default/src/test/resources/ValidationMessages.properties b/integration-tests/locales/default/src/test/resources/ValidationMessages.properties new file mode 100644 index 00000000000000..48a3bbf23dce15 --- /dev/null +++ b/integration-tests/locales/default/src/test/resources/ValidationMessages.properties @@ -0,0 +1 @@ +pattern.message=Value is not in line with the pattern diff --git a/integration-tests/locales/default/src/test/resources/ValidationMessages_fr_FR.properties b/integration-tests/locales/default/src/test/resources/ValidationMessages_fr_FR.properties new file mode 100644 index 00000000000000..c9a6e5d71d6f0a --- /dev/null +++ b/integration-tests/locales/default/src/test/resources/ValidationMessages_fr_FR.properties @@ -0,0 +1 @@ +pattern.message=La valeur ne correspond pas à l'échantillon diff --git a/integration-tests/locales/default/src/test/resources/ValidationMessages_hr_HR.properties b/integration-tests/locales/default/src/test/resources/ValidationMessages_hr_HR.properties new file mode 100644 index 00000000000000..ae2e444a981058 --- /dev/null +++ b/integration-tests/locales/default/src/test/resources/ValidationMessages_hr_HR.properties @@ -0,0 +1 @@ +pattern.message=Vrijednost ne zadovoljava uzorak diff --git a/integration-tests/locales/default/src/test/resources/application.properties b/integration-tests/locales/default/src/test/resources/application.properties new file mode 100644 index 00000000000000..d0d560bcb83361 --- /dev/null +++ b/integration-tests/locales/default/src/test/resources/application.properties @@ -0,0 +1,4 @@ +quarkus.locales=de,fr-FR,ja,uk-UA +# Note that quarkus.native.user-language is deprecated and solely quarkus.default-locale should be +# used in your application properties. This test uses it only to verify compatibility. +quarkus.default-locale=mt-MT diff --git a/integration-tests/locales/pom.xml b/integration-tests/locales/pom.xml index 0478b1166eef59..08dc1763e4ba0e 100644 --- a/integration-tests/locales/pom.xml +++ b/integration-tests/locales/pom.xml @@ -14,6 +14,7 @@ app all + default some diff --git a/integration-tests/locales/some/src/test/java/io/quarkus/locales/it/LocalesIT.java b/integration-tests/locales/some/src/test/java/io/quarkus/locales/it/LocalesIT.java index 4b192ba4ca03af..dab72bc4f499b7 100644 --- a/integration-tests/locales/some/src/test/java/io/quarkus/locales/it/LocalesIT.java +++ b/integration-tests/locales/some/src/test/java/io/quarkus/locales/it/LocalesIT.java @@ -10,6 +10,9 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import io.quarkus.test.junit.DisableIfBuiltWithGraalVMNewerThan; +import io.quarkus.test.junit.DisableIfBuiltWithGraalVMOlderThan; +import io.quarkus.test.junit.GraalVMVersion; import io.quarkus.test.junit.QuarkusIntegrationTest; import io.restassured.RestAssured; @@ -81,7 +84,8 @@ public void testTimeZones(String zone, String language, String name) { } @Test - public void testDefaultLocale() { + @DisableIfBuiltWithGraalVMNewerThan(value = GraalVMVersion.GRAALVM_24_1_0) + public void testDefaultLocaleBefore24_2() { RestAssured.given().when() .get("/default/de-CH") .then() @@ -94,6 +98,21 @@ public void testDefaultLocale() { .log().all(); } + @Test + @DisableIfBuiltWithGraalVMOlderThan(value = GraalVMVersion.GRAALVM_24_2_0) + public void testDefaultLocaleAfter24_1() { + RestAssured.given().when() + .get("/default/de-CH") + .then() + .statusCode(HttpStatus.SC_OK) + /* + * "Schweiz" is the correct name for Switzerland in German. + * German is the default language as per the `quarkus.test.arg-line` in application.properties. + */ + .body(is("Schweiz")) + .log().all(); + } + @Test public void testMissingLocaleSorryItaly() { RestAssured.given().when() diff --git a/integration-tests/locales/some/src/test/resources/application.properties b/integration-tests/locales/some/src/test/resources/application.properties index 4d3a429e5788c6..4617c6481b6d1e 100644 --- a/integration-tests/locales/some/src/test/resources/application.properties +++ b/integration-tests/locales/some/src/test/resources/application.properties @@ -3,3 +3,4 @@ quarkus.locales=de,fr-FR,ja,uk-UA # used in your application properties. This test uses it only to verify compatibility. quarkus.native.user-language=cs quarkus.default-locale=en-US +quarkus.test.arg-line=-Duser.language=de \ No newline at end of file diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/GraalVMVersion.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/GraalVMVersion.java index 3cf037b1f83abf..0b06c693126ccc 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/GraalVMVersion.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/GraalVMVersion.java @@ -6,7 +6,9 @@ public enum GraalVMVersion { GRAALVM_23_1_2(GraalVM.Version.VERSION_23_1_2), GRAALVM_23_1_3(GraalVM.Version.VERSION_23_1_3), GRAALVM_24_0_0(GraalVM.Version.VERSION_24_0_0), - GRAALVM_24_0_999(GraalVM.Version.VERSION_24_0_999); + GRAALVM_24_0_999(GraalVM.Version.VERSION_24_0_999), + GRAALVM_24_1_0(GraalVM.Version.VERSION_24_1_0), + GRAALVM_24_2_0(GraalVM.Version.VERSION_24_2_0); private final GraalVM.Version version;