From 7f7d3d0859eb50d4e1182b0d20d26120f129e440 Mon Sep 17 00:00:00 2001 From: Foivos Zakkak Date: Mon, 23 Sep 2024 18:14:06 +0300 Subject: [PATCH] Adapt locales support for GraalVM >= 24.2 Starting with GraalVM for JDK 24 (24.2) native image will no longer set the locale default at build time. As a result, the default locale won't be included by default in the native image unless explicitly specified. As discussed in https://github.com/quarkusio/quarkus/discussions/43533#discussioncomment-10797019 this patch updates the locales support so that: - if neither `quarkus.locales` nor `quarkus.default-locale` is set, the Quarkus applications should default to English (`en_US`), instead of the build systems locale (which is the current behavior), at run-time. - if `quarkus.default-locale` is set but `quarkus.locales` is not set, then we should only include the locale `quarkus.default-locale` is set to. This is the current behavior with GraalVM for JDK 21. - if both `quarkus.default-locale` and `quarkus.locales` are set, then we should include only the locales from `quarkus.locales` and the one from `quarkus.default-locale` (this is the current behavior). - if `quarkus.locales` is set but `quarkus.default-locale` is not set, then we should include only the locales from `quarkus.locales` and default to English, instead of the build systems locale (which is the current behavior), at run-time (similarly to point 1). - if `quarkus.default-locale` (which is build time fixed) is set, it is used to set the default `user.language` and `user.country` values at run-time, while users may still override them. For points 2 and 3 starting with graalVM for JDK 24 we also include `en_US` which shouldn't be a big issue as mentioned in https://github.com/quarkusio/quarkus/discussions/43533#discussioncomment-10795928, CAUTION: Point 1 changes the current behavior, meaning we need to clearly document and communicate it. This patch also updates the Locales integration tests accordingly. See https://github.com/oracle/graal/pull/9694 --- .github/native-tests.json | 2 +- .../quarkus/deployment/pkg/NativeConfig.java | 6 +- .../quarkus/deployment/pkg/steps/GraalVM.java | 2 + .../pkg/steps/NativeImageBuildStep.java | 9 -- .../deployment/steps/LocaleProcessor.java | 50 ++++++++-- .../steps/NativeImageFeatureStep.java | 48 ++++++++- .../runtime/LocalesBuildTimeConfig.java | 9 +- .../runtime/HibernateValidatorRecorder.java | 2 +- .../deployment/MessageBundleProcessor.java | 3 +- .../quarkus/qute/runtime/EngineProducer.java | 2 +- .../java/io/quarkus/locales/it/LocalesIT.java | 32 ++++++ .../src/test/resources/application.properties | 1 + integration-tests/locales/default/pom.xml | 97 +++++++++++++++++++ .../locales/it/DefaultLocaleResource.java | 26 +++++ .../java/io/quarkus/locales/it/LocalesIT.java | 59 +++++++++++ .../io/quarkus/locales/it/LocalesTest.java | 7 ++ .../resources/ValidationMessages.properties | 1 + .../ValidationMessages_fr_FR.properties | 1 + .../ValidationMessages_hr_HR.properties | 1 + .../src/test/resources/application.properties | 4 + integration-tests/locales/pom.xml | 1 + .../java/io/quarkus/locales/it/LocalesIT.java | 30 ++++-- .../src/test/resources/application.properties | 2 + .../io/quarkus/test/junit/GraalVMVersion.java | 4 +- 24 files changed, 358 insertions(+), 41 deletions(-) create mode 100644 integration-tests/locales/default/pom.xml create mode 100644 integration-tests/locales/default/src/main/java/io/quarkus/locales/it/DefaultLocaleResource.java create mode 100644 integration-tests/locales/default/src/test/java/io/quarkus/locales/it/LocalesIT.java create mode 100644 integration-tests/locales/default/src/test/java/io/quarkus/locales/it/LocalesTest.java create mode 100644 integration-tests/locales/default/src/test/resources/ValidationMessages.properties create mode 100644 integration-tests/locales/default/src/test/resources/ValidationMessages_fr_FR.properties create mode 100644 integration-tests/locales/default/src/test/resources/ValidationMessages_hr_HR.properties create mode 100644 integration-tests/locales/default/src/test/resources/application.properties diff --git a/.github/native-tests.json b/.github/native-tests.json index f7cdd076826dc..631bc2fa21730 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 12c4728038d71..d3fc8972f635e 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 21113760e3c4b..89c620d6f7c45 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 @@ -191,6 +191,8 @@ public static final class Version extends io.quarkus.runtime.graal.GraalVM.Versi 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_1_999 = new Version("GraalVM 24.1.999", "24.1.999", "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 287b61399b985..29a287fac687f 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 @@ -746,15 +746,6 @@ public NativeImageInvokerInfo build() { } } } - - final String userLanguage = LocaleProcessor.nativeImageUserLanguage(nativeConfig, localesBuildTimeConfig); - if (!userLanguage.isEmpty()) { - nativeImageArgs.add("-J-Duser.language=" + userLanguage); - } - final String userCountry = LocaleProcessor.nativeImageUserCountry(nativeConfig, localesBuildTimeConfig); - if (!userCountry.isEmpty()) { - nativeImageArgs.add("-J-Duser.country=" + userCountry); - } final String includeLocales = LocaleProcessor.nativeImageIncludeLocales(nativeConfig, localesBuildTimeConfig); if (!includeLocales.isEmpty()) { if ("all".equals(includeLocales)) { 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 8db1bafb7a109..ba3062e7ae52e 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 @@ -14,6 +14,7 @@ import io.quarkus.deployment.builditem.GeneratedResourceBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBundleBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageSystemPropertyBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.pkg.NativeConfig; import io.quarkus.deployment.pkg.steps.NativeBuild; @@ -59,6 +60,23 @@ void servicesResource(BuildProducer nativeImageRes "sun.util.resources.provider.LocaleDataProvider".getBytes(StandardCharsets.UTF_8))); } + /** + * These exports are only required for GraalVM for JDK < 24, but don't cause any issues for newer versions. + * To be removed once we drop support for GraalVM for JDK < 24. + */ + @BuildStep(onlyIf = NativeBuild.class) + void setDefaults(BuildProducer buildtimeSystemProperties, + NativeConfig nativeConfig, LocalesBuildTimeConfig localesBuildTimeConfig) { + String language = nativeImageUserLanguage(nativeConfig, localesBuildTimeConfig); + if (!language.isEmpty()) { + buildtimeSystemProperties.produce(new NativeImageSystemPropertyBuildItem("user.language", language)); + } + String country = nativeImageUserCountry(nativeConfig, localesBuildTimeConfig); + if (!country.isEmpty()) { + buildtimeSystemProperties.produce(new NativeImageSystemPropertyBuildItem("user.country", country)); + } + } + /** * We activate additional resources in native-image executable only if user opts * for anything else than what is already the system default. @@ -80,7 +98,8 @@ public boolean getAsBoolean() { (nativeConfig.userCountry().isPresent() && !Locale.getDefault().getCountry().equals(nativeConfig.userCountry().get())) || - !Locale.getDefault().equals(localesBuildTimeConfig.defaultLocale) + (localesBuildTimeConfig.defaultLocale.isPresent() && + !Locale.getDefault().equals(localesBuildTimeConfig.defaultLocale.get())) || localesBuildTimeConfig.locales.stream().anyMatch(l -> !Locale.getDefault().equals(l)); } @@ -93,9 +112,14 @@ public boolean getAsBoolean() { * @param localesBuildTimeConfig * @return User language set by 'quarkus.default-locale' or by deprecated 'quarkus.native.user-language' or * effectively LocalesBuildTimeConfig.DEFAULT_LANGUAGE if none of the aforementioned is set. + * @Deprecated */ + @Deprecated public static String nativeImageUserLanguage(NativeConfig nativeConfig, LocalesBuildTimeConfig localesBuildTimeConfig) { - String language = localesBuildTimeConfig.defaultLocale.getLanguage(); + String language = System.getProperty("user.language", "en"); + if (localesBuildTimeConfig.defaultLocale.isPresent()) { + language = localesBuildTimeConfig.defaultLocale.get().getLanguage(); + } if (nativeConfig.userLanguage().isPresent()) { log.warn(DEPRECATED_USER_LANGUAGE_WARNING); // The deprecated option takes precedence for users who are already using it. @@ -112,9 +136,14 @@ public static String nativeImageUserLanguage(NativeConfig nativeConfig, LocalesB * @return User country set by 'quarkus.default-locale' or by deprecated 'quarkus.native.user-country' or * effectively LocalesBuildTimeConfig.DEFAULT_COUNTRY (could be an empty string) if none of the aforementioned is * set. + * @Deprecated */ + @Deprecated public static String nativeImageUserCountry(NativeConfig nativeConfig, LocalesBuildTimeConfig localesBuildTimeConfig) { - String country = localesBuildTimeConfig.defaultLocale.getCountry(); + String country = System.getProperty("user.country", ""); + if (localesBuildTimeConfig.defaultLocale.isPresent()) { + country = localesBuildTimeConfig.defaultLocale.get().getCountry(); + } if (nativeConfig.userCountry().isPresent()) { log.warn(DEPRECATED_USER_COUNTRY_WARNING); // The deprecated option takes precedence for users who are already using it. @@ -124,7 +153,7 @@ public static String nativeImageUserCountry(NativeConfig nativeConfig, LocalesBu } /** - * Additional locales to be included in native-image executable. + * Locales to be included in native-image executable. * * @param nativeConfig * @param localesBuildTimeConfig @@ -139,17 +168,18 @@ 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 if set by the user. + // 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 if (localesBuildTimeConfig.defaultLocale.isPresent()) { + additionalLocales.add(localesBuildTimeConfig.defaultLocale.get()); } return additionalLocales.stream() - .filter(l -> !Locale.getDefault().equals(l)) .map(l -> l.getLanguage() + (l.getCountry().isEmpty() ? "" : "-" + l.getCountry())) .collect(Collectors.joining(",")); } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageFeatureStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageFeatureStep.java index 97ade7fea8806..057d5d3ee9c02 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageFeatureStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageFeatureStep.java @@ -9,6 +9,7 @@ import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.hosted.Feature; import org.graalvm.nativeimage.hosted.RuntimeClassInitialization; +import org.graalvm.nativeimage.hosted.RuntimeSystemProperties; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; @@ -18,6 +19,9 @@ import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedPackageBuildItem; import io.quarkus.deployment.builditem.nativeimage.RuntimeReinitializedClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.UnsafeAccessedFieldBuildItem; +import io.quarkus.deployment.pkg.NativeConfig; +import io.quarkus.gizmo.BranchResult; +import io.quarkus.gizmo.BytecodeCreator; import io.quarkus.gizmo.CatchBlockCreator; import io.quarkus.gizmo.ClassCreator; import io.quarkus.gizmo.ClassOutput; @@ -25,6 +29,7 @@ import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; import io.quarkus.gizmo.TryBlock; +import io.quarkus.runtime.LocalesBuildTimeConfig; import io.quarkus.runtime.graal.GraalVM; public class NativeImageFeatureStep { @@ -35,6 +40,12 @@ public class NativeImageFeatureStep { Class.class); private static final MethodDescriptor BUILD_TIME_INITIALIZATION = ofMethod(RuntimeClassInitialization.class, "initializeAtBuildTime", void.class, String[].class); + private static final MethodDescriptor REGISTER_RUNTIME_SYSTEM_PROPERTIES = ofMethod(RuntimeSystemProperties.class, + "register", void.class, String.class, String.class); + private static final MethodDescriptor GRAALVM_VERSION_GET_CURRENT = ofMethod(GraalVM.Version.class, "getCurrent", + GraalVM.Version.class); + private static final MethodDescriptor GRAALVM_VERSION_COMPARE_TO = ofMethod(GraalVM.Version.class, "compareTo", int.class, + int[].class); private static final MethodDescriptor INITIALIZE_CLASSES_AT_RUN_TIME = ofMethod(RuntimeClassInitialization.class, "initializeAtRunTime", void.class, Class[].class); private static final MethodDescriptor INITIALIZE_PACKAGES_AT_RUN_TIME = ofMethod(RuntimeClassInitialization.class, @@ -58,11 +69,12 @@ void addExportsToNativeImage(BuildProducer features) { @BuildStep void generateFeature(BuildProducer nativeImageClass, - BuildProducer exports, List runtimeInitializedClassBuildItems, List runtimeInitializedPackageBuildItems, List runtimeReinitializedClassBuildItems, - List unsafeAccessedFields) { + List unsafeAccessedFields, + NativeConfig nativeConfig, + LocalesBuildTimeConfig localesBuildTimeConfig) { ClassCreator file = new ClassCreator(new ClassOutput() { @Override public void write(String s, byte[] bytes) { @@ -81,6 +93,38 @@ public void write(String s, byte[] bytes) { overallCatch.invokeStaticMethod(BUILD_TIME_INITIALIZATION, overallCatch.marshalAsArray(String.class, overallCatch.load(""))); // empty string means initialize everything + // Set the user.language and user.country system properties to the default locale + // The deprecated option takes precedence for users who are already using it. + if (nativeConfig.userLanguage().isPresent()) { + overallCatch.invokeStaticMethod(REGISTER_RUNTIME_SYSTEM_PROPERTIES, + overallCatch.load("user.language"), overallCatch.load(nativeConfig.userLanguage().get())); + if (nativeConfig.userCountry().isPresent()) { + overallCatch.invokeStaticMethod(REGISTER_RUNTIME_SYSTEM_PROPERTIES, + overallCatch.load("user.country"), overallCatch.load(nativeConfig.userCountry().get())); + } + } else if (localesBuildTimeConfig.defaultLocale.isPresent()) { + overallCatch.invokeStaticMethod(REGISTER_RUNTIME_SYSTEM_PROPERTIES, + overallCatch.load("user.language"), + overallCatch.load(localesBuildTimeConfig.defaultLocale.get().getLanguage())); + overallCatch.invokeStaticMethod(REGISTER_RUNTIME_SYSTEM_PROPERTIES, + overallCatch.load("user.country"), + overallCatch.load(localesBuildTimeConfig.defaultLocale.get().getCountry())); + } else { + ResultHandle graalVMVersion = overallCatch.invokeStaticMethod(GRAALVM_VERSION_GET_CURRENT); + BranchResult graalVm24_2Test = overallCatch + .ifGreaterEqualZero(overallCatch.invokeVirtualMethod(GRAALVM_VERSION_COMPARE_TO, graalVMVersion, + overallCatch.marshalAsArray(int.class, overallCatch.load(24), overallCatch.load(2)))); + /* GraalVM >= 24.2 */ + try (BytecodeCreator greaterEqual24_2 = graalVm24_2Test.trueBranch()) { + greaterEqual24_2.invokeStaticMethod(REGISTER_RUNTIME_SYSTEM_PROPERTIES, + greaterEqual24_2.load("user.language"), + greaterEqual24_2.load("en")); + greaterEqual24_2.invokeStaticMethod(REGISTER_RUNTIME_SYSTEM_PROPERTIES, + greaterEqual24_2.load("user.country"), + greaterEqual24_2.load("US")); + } + } + if (!runtimeInitializedClassBuildItems.isEmpty()) { // Class[] runtimeInitializedClasses() MethodCreator runtimeInitializedClasses = file 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 7ca78d84ceeb0..1afd1e0a80742 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/LocalesBuildTimeConfig.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/LocalesBuildTimeConfig.java @@ -1,6 +1,7 @@ package io.quarkus.runtime; import java.util.Locale; +import java.util.Optional; import java.util.Set; import io.quarkus.runtime.annotations.ConfigDocPrefix; @@ -44,8 +45,10 @@ 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 {@code user.language} and {@code user.country} can also be overridden at + * runtime, provided the selected locale was included at image build time. */ - @ConfigItem(defaultValue = DEFAULT_LANGUAGE + "-" + DEFAULT_COUNTRY, defaultValueDocumentation = "Build system locale") - public Locale defaultLocale; + @ConfigItem(defaultValueDocumentation = "Defaults to the JVM's default locale if not set. " + + "Starting with GraalVM for JDK 24, it defaults to en-US for native executables.") + public Optional defaultLocale; } diff --git a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/HibernateValidatorRecorder.java b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/HibernateValidatorRecorder.java index baaafa562bfec..90a06fc13544a 100644 --- a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/HibernateValidatorRecorder.java +++ b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/HibernateValidatorRecorder.java @@ -86,7 +86,7 @@ public void created(BeanContainer container) { // Locales, Locale ROOT means all locales in this setting. .locales(localesBuildTimeConfig.locales.contains(Locale.ROOT) ? Set.of(Locale.getAvailableLocales()) : localesBuildTimeConfig.locales) - .defaultLocale(localesBuildTimeConfig.defaultLocale) + .defaultLocale(localesBuildTimeConfig.defaultLocale.orElse(Locale.getDefault())) .beanMetaDataClassNormalizer(new ArcProxyBeanMetaDataClassNormalizer()); if (hibernateValidatorBuildTimeConfig.expressionLanguage().constraintExpressionFeatureLevel().isPresent()) { diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java index 8b3af1267819e..f67dd11dbc181 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java @@ -18,6 +18,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.ListIterator; +import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Set; @@ -1417,7 +1418,7 @@ private String getDefaultLocale(AnnotationInstance bundleAnnotation, LocalesBuil AnnotationValue localeValue = bundleAnnotation.value(BUNDLE_LOCALE); String defaultLocale; if (localeValue == null || localeValue.asString().equals(MessageBundle.DEFAULT_LOCALE)) { - defaultLocale = locales.defaultLocale.toLanguageTag(); + defaultLocale = locales.defaultLocale.orElse(Locale.getDefault()).toLanguageTag(); } else { defaultLocale = localeValue.asString(); } diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java index f6bbc3957396d..ccf810e0da184 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java @@ -97,7 +97,7 @@ public EngineProducer(QuteContext context, QuteConfig config, QuteRuntimeConfig this.templateContents = Map.copyOf(context.getTemplateContents()); this.tags = context.getTags(); this.templatePathExclude = config.templatePathExclude; - this.defaultLocale = locales.defaultLocale; + this.defaultLocale = locales.defaultLocale.orElse(Locale.getDefault()); this.defaultCharset = config.defaultCharset; this.container = Arc.container(); diff --git a/integration-tests/locales/all/src/test/java/io/quarkus/locales/it/LocalesIT.java b/integration-tests/locales/all/src/test/java/io/quarkus/locales/it/LocalesIT.java index 2fba39329a6ea..b08dd85b66ef4 100644 --- a/integration-tests/locales/all/src/test/java/io/quarkus/locales/it/LocalesIT.java +++ b/integration-tests/locales/all/src/test/java/io/quarkus/locales/it/LocalesIT.java @@ -4,12 +4,17 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import java.util.Locale; + 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; @@ -125,4 +130,31 @@ public void testValidationMessageLocale(String acceptLanguage, String expectedMe .then() .body(containsString(expectedMessage)); } + + // This test works best in a non-english locale. + @Test + @DisableIfBuiltWithGraalVMNewerThan(value = GraalVMVersion.GRAALVM_24_1_999) + public void testDefaultLocalePre24_2() { + RestAssured.given().when() + .get("/default/de-CH") + .then() + .statusCode(HttpStatus.SC_OK) + // With GraalVM < 24.2, the default locale is picked up by the build system when not set by the user. + .body(is(Locale.forLanguageTag("de-CH").getDisplayCountry())) + .log().all(); + } + + // This test works best in a non-english locale. + @Test + @DisableIfBuiltWithGraalVMOlderThan(value = GraalVMVersion.GRAALVM_24_2_0) + public void testDefaultLocalePost24_1() { + RestAssured.given().when() + .get("/default/de-CH") + .then() + .statusCode(HttpStatus.SC_OK) + // Starting with GraalVM 24.2, the default locale is en-US when not set by the user. + .body(is("Switzerland")) + .log().all(); + } + } diff --git a/integration-tests/locales/all/src/test/resources/application.properties b/integration-tests/locales/all/src/test/resources/application.properties index acc6621c863f2..3344cc17ceb05 100644 --- a/integration-tests/locales/all/src/test/resources/application.properties +++ b/integration-tests/locales/all/src/test/resources/application.properties @@ -1,2 +1,3 @@ quarkus.locales=all quarkus.native.resources.includes=AppMessages_*.properties +quarkus.test.env.LC_ALL=mt_MT.UTF-8 \ No newline at end of file diff --git a/integration-tests/locales/default/pom.xml b/integration-tests/locales/default/pom.xml new file mode 100644 index 0000000000000..2d635a1715b6b --- /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 0000000000000..3e01732b91e52 --- /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 0000000000000..84a16611fef28 --- /dev/null +++ b/integration-tests/locales/default/src/test/java/io/quarkus/locales/it/LocalesIT.java @@ -0,0 +1,59 @@ +package io.quarkus.locales.it; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; + +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +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 { + + @Test + public void testDefaultLocale() { + 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(); + } + + /** + * @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 0000000000000..bd42cd4ae7fe7 --- /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 0000000000000..48a3bbf23dce1 --- /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 0000000000000..c9a6e5d71d6f0 --- /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 0000000000000..ae2e444a98105 --- /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 0000000000000..d0d560bcb8336 --- /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 0478b1166eef5..08dc1763e4ba0 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 4dad3f2d0a8fc..3f062fcd844ae 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 @@ -11,6 +11,7 @@ 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; @@ -43,9 +44,6 @@ public void testCorrectLocales(String country, String language, String translati .log().all(); } - // Disable test with GraalVM 24.2 for JDK 24 and later till we reach a conclusion in - // https://github.com/quarkusio/quarkus/discussions/43533 - @DisableIfBuiltWithGraalVMNewerThan(value = GraalVMVersion.GRAALVM_24_1_0) @ParameterizedTest @CsvSource(value = { "en-US|en|US Dollar", @@ -65,9 +63,6 @@ public void testCurrencies(String country, String language, String currency) { .log().all(); } - // Disable test with GraalVM 24.2 for JDK 24 and later till we reach a conclusion in - // https://github.com/quarkusio/quarkus/discussions/43533 - @DisableIfBuiltWithGraalVMNewerThan(value = GraalVMVersion.GRAALVM_24_1_0) @ParameterizedTest @CsvSource(value = { "Asia/Tokyo|fr|heure normale du Japon", @@ -88,16 +83,15 @@ public void testTimeZones(String zone, String language, String name) { .log().all(); } - // Disable test with GraalVM 24.2 for JDK 24 and later till we reach a conclusion in - // https://github.com/quarkusio/quarkus/discussions/43533 @Test - @DisableIfBuiltWithGraalVMNewerThan(value = GraalVMVersion.GRAALVM_24_1_0) - public void testDefaultLocale() { + @DisableIfBuiltWithGraalVMNewerThan(value = GraalVMVersion.GRAALVM_24_1_999) + public void testDefaultLocalePre24_2() { RestAssured.given().when() .get("/default/de-CH") .then() .statusCode(HttpStatus.SC_OK) /* + * Prior to GraalVM 24.2, the locale could not be changed at runtime. * "Švýcarsko" is the correct name for Switzerland in Czech language. * Czech is the default language as per quarkus.native.user-language=cs. */ @@ -105,6 +99,22 @@ public void testDefaultLocale() { .log().all(); } + @Test + @DisableIfBuiltWithGraalVMOlderThan(value = GraalVMVersion.GRAALVM_24_2_0) + public void testDefaultLocalePost24_1() { + RestAssured.given().when() + .get("/default/de-CH") + .then() + .statusCode(HttpStatus.SC_OK) + /* + * Starting with GraalVM 24.2, the locale can be set at runtime. + * "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 4d3a429e5788c..d5f80d916383d 100644 --- a/integration-tests/locales/some/src/test/resources/application.properties +++ b/integration-tests/locales/some/src/test/resources/application.properties @@ -3,3 +3,5 @@ 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 +quarkus.test.env.LC_ALL=mt_MT.UTF-8 \ 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 7110ce6c275b3..7a647525390d7 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 @@ -7,7 +7,9 @@ public enum GraalVMVersion { 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_1_0(GraalVM.Version.VERSION_24_1_0); + GRAALVM_24_1_0(GraalVM.Version.VERSION_24_1_0), + GRAALVM_24_1_999(GraalVM.Version.VERSION_24_1_999), + GRAALVM_24_2_0(GraalVM.Version.VERSION_24_2_0); private final GraalVM.Version version;