From c025a1b29c02b6a4b8b773d6041c9e50119147a0 Mon Sep 17 00:00:00 2001 From: Michal Karm Babacek Date: Wed, 15 Nov 2023 14:11:11 +0100 Subject: [PATCH] Introduces quarkus.locales=all Hibernate validator interprets Locale.ROOT as array of all Fixes hibernate-validator for quarkus.locales=all --- .../pkg/steps/NativeImageBuildStep.java | 13 +- .../deployment/steps/LocaleProcessor.java | 6 + .../runtime/LocalesBuildTimeConfig.java | 3 + .../configuration/LocaleConverter.java | 6 +- docs/src/main/asciidoc/validation.adoc | 3 + .../runtime/HibernateValidatorRecorder.java | 8 +- integration-tests/locales/README.md | 16 +++ integration-tests/locales/all/pom.xml | 100 ++++++++++++++ .../locales/it/AllLocalesResource.java | 26 ++++ .../java/io/quarkus/locales/it/LocalesIT.java | 128 ++++++++++++++++++ .../io/quarkus/locales/it/LocalesTest.java | 0 .../test/resources/AppMessages_cs.properties | 1 + .../test/resources/AppMessages_en.properties | 1 + .../test/resources/AppMessages_uk.properties | 1 + .../resources/ValidationMessages.properties | 1 + .../ValidationMessages_hr_HR.properties | 1 + .../ValidationMessages_uk_UA.properties | 1 + .../ValidationMessages_zh.properties | 1 + .../src/test/resources/application.properties | 2 + integration-tests/locales/app/pom.xml | 70 ++++++++++ .../quarkus/locales/it/LocalesResource.java | 78 +++++++++++ integration-tests/locales/pom.xml | 89 +----------- integration-tests/locales/some/pom.xml | 99 ++++++++++++++ .../locales/it/SomeLocalesResource.java | 26 ++++ .../java/io/quarkus/locales/it/LocalesIT.java | 78 +++++++---- .../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 | 2 +- .../quarkus/locales/it/LocalesResource.java | 46 ------- 31 files changed, 649 insertions(+), 167 deletions(-) create mode 100644 integration-tests/locales/README.md create mode 100644 integration-tests/locales/all/pom.xml create mode 100644 integration-tests/locales/all/src/main/java/io/quarkus/locales/it/AllLocalesResource.java create mode 100644 integration-tests/locales/all/src/test/java/io/quarkus/locales/it/LocalesIT.java rename integration-tests/locales/{ => all}/src/test/java/io/quarkus/locales/it/LocalesTest.java (100%) create mode 100644 integration-tests/locales/all/src/test/resources/AppMessages_cs.properties create mode 100644 integration-tests/locales/all/src/test/resources/AppMessages_en.properties create mode 100644 integration-tests/locales/all/src/test/resources/AppMessages_uk.properties create mode 100644 integration-tests/locales/all/src/test/resources/ValidationMessages.properties create mode 100644 integration-tests/locales/all/src/test/resources/ValidationMessages_hr_HR.properties create mode 100644 integration-tests/locales/all/src/test/resources/ValidationMessages_uk_UA.properties create mode 100644 integration-tests/locales/all/src/test/resources/ValidationMessages_zh.properties create mode 100644 integration-tests/locales/all/src/test/resources/application.properties create mode 100644 integration-tests/locales/app/pom.xml create mode 100644 integration-tests/locales/app/src/main/java/io/quarkus/locales/it/LocalesResource.java create mode 100644 integration-tests/locales/some/pom.xml create mode 100644 integration-tests/locales/some/src/main/java/io/quarkus/locales/it/SomeLocalesResource.java rename integration-tests/locales/{ => some}/src/test/java/io/quarkus/locales/it/LocalesIT.java (58%) create mode 100644 integration-tests/locales/some/src/test/java/io/quarkus/locales/it/LocalesTest.java create mode 100644 integration-tests/locales/some/src/test/resources/ValidationMessages.properties create mode 100644 integration-tests/locales/some/src/test/resources/ValidationMessages_fr_FR.properties create mode 100644 integration-tests/locales/some/src/test/resources/ValidationMessages_hr_HR.properties rename integration-tests/locales/{ => some}/src/test/resources/application.properties (87%) delete mode 100644 integration-tests/locales/src/main/java/io/quarkus/locales/it/LocalesResource.java 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 33027514c089d..ccdd7ae667682 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 @@ -348,8 +348,8 @@ public NativeImageRunnerBuildItem resolveNativeImageBuildRunner(NativeConfig nat } /** - * Creates a dummy runner for native-sources builds. This allows the creation of native-source jars without - * requiring podman/docker or a local native-image installation. + * Creates a dummy runner for native-sources builds. This allows the creation of native-source jars without requiring + * podman/docker or a local native-image installation. */ @BuildStep(onlyIf = NativeSourcesBuild.class) public NativeImageRunnerBuildItem dummyNativeImageBuildRunner(NativeConfig nativeConfig) { @@ -725,7 +725,14 @@ public NativeImageInvokerInfo build() { } final String includeLocales = LocaleProcessor.nativeImageIncludeLocales(nativeConfig, localesBuildTimeConfig); if (!includeLocales.isEmpty()) { - addExperimentalVMOption(nativeImageArgs, "-H:IncludeLocales=" + includeLocales); + if ("all".equals(includeLocales)) { + log.warn( + "Your application is setting the 'quarkus.locales' configuration key to include 'all'. " + + "All JDK locales, languages, currencies, etc. will be included, inflating the size of the executable."); + addExperimentalVMOption(nativeImageArgs, "-H:+IncludeAllLocales"); + } else { + addExperimentalVMOption(nativeImageArgs, "-H:IncludeLocales=" + includeLocales); + } } nativeImageArgs.add("-J-Dfile.encoding=" + nativeConfig.fileEncoding()); 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 14d9e85ed373b..8db1bafb7a109 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 @@ -129,10 +129,16 @@ public static String nativeImageUserCountry(NativeConfig nativeConfig, LocalesBu * @param nativeConfig * @param localesBuildTimeConfig * @return A comma separated list of IETF BCP 47 language tags, optionally with ISO 3166-1 alpha-2 country codes. + * As a special case a string "all" making the native-image to include all available locales. */ public static String nativeImageIncludeLocales(NativeConfig nativeConfig, LocalesBuildTimeConfig localesBuildTimeConfig) { // We start with what user sets as needed locales final Set additionalLocales = new HashSet<>(localesBuildTimeConfig.locales); + + if (additionalLocales.contains(Locale.ROOT)) { + 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); 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 89bc96bcff0c8..deb6fd4dae7a3 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/LocalesBuildTimeConfig.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/LocalesBuildTimeConfig.java @@ -23,6 +23,9 @@ public class LocalesBuildTimeConfig { *

* Native-image build uses it to define additional locales that are supposed * to be available at runtime. + *

+ * A special string "all" is translated as ROOT Locale and then used in native-image + * to include all locales. Image size penalty applies. */ @ConfigItem(defaultValue = DEFAULT_LANGUAGE + "-" + DEFAULT_COUNTRY, defaultValueDocumentation = "Set containing the build system locale") diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/LocaleConverter.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/LocaleConverter.java index 8a5e83a0512bb..ac70e88fbc622 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/LocaleConverter.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/LocaleConverter.java @@ -23,12 +23,16 @@ public LocaleConverter() { @Override public Locale convert(final String value) { - String localeValue = value.trim(); + final String localeValue = value.trim(); if (localeValue.isEmpty()) { return null; } + if ("all".equals(localeValue)) { + return Locale.ROOT; + } + Locale locale = Locale.forLanguageTag(NORMALIZE_LOCALE_PATTERN.matcher(localeValue).replaceAll("-")); if (locale != Locale.ROOT && (locale.getLanguage() == null || locale.getLanguage().isEmpty())) { throw new IllegalArgumentException("Unable to resolve locale: " + value); diff --git a/docs/src/main/asciidoc/validation.adoc b/docs/src/main/asciidoc/validation.adoc index 57f0e1f40087e..26798ae318aa2 100644 --- a/docs/src/main/asciidoc/validation.adoc +++ b/docs/src/main/asciidoc/validation.adoc @@ -397,6 +397,9 @@ provided the supported locales have been properly specified in the `application. quarkus.locales=en-US,es-ES,fr-FR ---- +Alternatively, you can use `all` to make native-image executable to include all available locales. It inflate the size of the executable +substantially though. The difference between including just two or three locales and including all locales is at least 23 MB. + A similar mechanism exists for GraphQL services based on the `quarkus-smallrye-graphql` extension. If this default mechanism is not sufficient and you need a custom locale resolution, you can add additional ``org.hibernate.validator.spi.messageinterpolation.LocaleResolver``s: 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 5888318baae13..88b808fd14142 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 @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Set; import java.util.function.Supplier; @@ -75,10 +76,11 @@ public void created(BeanContainer container) { configuration.localeResolver(localeResolver); } - configuration - .builtinConstraints(detectedBuiltinConstraints) + configuration.builtinConstraints(detectedBuiltinConstraints) .initializeBeanMetaData(classesToBeValidated) - .locales(localesBuildTimeConfig.locales) + // Locales, Locale ROOT means all locales in this setting. + .locales(localesBuildTimeConfig.locales.contains(Locale.ROOT) ? Set.of(Locale.getAvailableLocales()) + : localesBuildTimeConfig.locales) .defaultLocale(localesBuildTimeConfig.defaultLocale) .beanMetaDataClassNormalizer(new ArcProxyBeanMetaDataClassNormalizer()); diff --git a/integration-tests/locales/README.md b/integration-tests/locales/README.md new file mode 100644 index 0000000000000..eb2b573d92219 --- /dev/null +++ b/integration-tests/locales/README.md @@ -0,0 +1,16 @@ +Locales (i18n) +============== + +Native-image built application does not have all [locales](https://docs.oracle.com/javase/tutorial/i18n/locale/index.html) included by default as it +unnecessarily inflates the executable size. + +One can configure native-image to [include locales](https://www.graalvm.org/latest/reference-manual/native-image/dynamic-features/Resources/#locales). This is mirrored in Quarkus configuration. + +All +--- +"All" test uses a special string "all" that internally translates as Locale.ROOT and is +interpreted as "Include all locales". + +Some +---- +"Some" test uses a list of picked locales and verifies that only those are available. diff --git a/integration-tests/locales/all/pom.xml b/integration-tests/locales/all/pom.xml new file mode 100644 index 0000000000000..1e7b627e295da --- /dev/null +++ b/integration-tests/locales/all/pom.xml @@ -0,0 +1,100 @@ + + + 4.0.0 + + io.quarkus + quarkus-integration-test-locales + 999-SNAPSHOT + + quarkus-integration-test-locales-all + Quarkus - Integration Tests - Locales - All + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-resteasy-reactive + + + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + io.quarkus + quarkus-integration-test-locales-app + 999-SNAPSHOT + compile + + + + + io.quarkus + quarkus-arc-deployment + ${project.version} + pom + test + + + * + * + + + + + + io.quarkus + quarkus-resteasy-reactive-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/all/src/main/java/io/quarkus/locales/it/AllLocalesResource.java b/integration-tests/locales/all/src/main/java/io/quarkus/locales/it/AllLocalesResource.java new file mode 100644 index 0000000000000..920a9c0be1152 --- /dev/null +++ b/integration-tests/locales/all/src/main/java/io/quarkus/locales/it/AllLocalesResource.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 AllLocalesResource extends LocalesResource { + private static final Logger LOG = Logger.getLogger(AllLocalesResource.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/all/src/test/java/io/quarkus/locales/it/LocalesIT.java b/integration-tests/locales/all/src/test/java/io/quarkus/locales/it/LocalesIT.java new file mode 100644 index 0000000000000..2fba39329a6ea --- /dev/null +++ b/integration-tests/locales/all/src/test/java/io/quarkus/locales/it/LocalesIT.java @@ -0,0 +1,128 @@ +package io.quarkus.locales.it; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +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.QuarkusIntegrationTest; +import io.restassured.RestAssured; + +/** + * A special case where we want to include all locales in our app. It must not matter which arbitrary locale we use, it must + * work here. + */ +@QuarkusIntegrationTest +public class LocalesIT { + + private static final Logger LOG = Logger.getLogger(LocalesIT.class); + + @ParameterizedTest + @CsvSource(value = { + "en-US|en|United States", + "de-DE|de|Deutschland", + "de-AT|en|Austria", + "de-DE|en|Germany", + "zh-cmn-Hans-CN|cs|Čína", + "zh-Hant-TW|cs|Tchaj-wan", + "ja-JP-JP-#u-ca-japanese|sg|Zapöon" + }, delimiter = '|') + public void testCorrectLocales(String country, String language, String translation) { + LOG.infof("Triggering test: Country: %s, Language: %s, Translation: %s", country, language, translation); + RestAssured.given().when() + .get(String.format("/locale/%s/%s", country, language)) + .then() + .statusCode(HttpStatus.SC_OK) + .body(is(translation)) + .log().all(); + } + + @Test + public void testItalyIncluded() { + RestAssured.given().when() + .get("/locale/it-IT/it") + .then() + .statusCode(HttpStatus.SC_OK) + .body(is("Italia")) + .log().all(); + } + + @ParameterizedTest + @CsvSource(value = { + "0,666|en-US|666.0", + "0,666|cs-CZ|0.666", + "0,666|fr-FR|0.666", + "0.666|fr-FR|0.0" + }, delimiter = '|') + public void testNumbers(String number, String locale, String expected) { + LOG.infof("Triggering test: Number: %s, Locale: %s, Expected result: %s", number, locale, expected); + RestAssured.given().when() + .param("number", number) + .param("locale", locale) + .get("/numbers") + .then() + .statusCode(HttpStatus.SC_OK) + .body(equalTo(expected)) + .log().all(); + } + + @Test + public void languageRanges() { + RestAssured.given().when() + .param("range", "Accept-Language:iw,en-us;q=0.7,en;q=0.3") + .get("/ranges") + .then() + .statusCode(HttpStatus.SC_OK) + .body(is("[iw, he, en-us;q=0.7, en;q=0.3]")) + .log().all(); + } + + @ParameterizedTest + @CsvSource(value = { + // Ukrainian language preference is higher than Czech. + "cs;q=0.7,uk;q=0.9|Привіт Світ!", + // Czech language preference is higher than Ukrainian. + "cs;q=1.0,uk;q=0.9|Ahoj světe!", + // An unknown language preference, silent fallback to lingua franca. + "jp;q=1.0|Hello world!" + }, delimiter = '|') + public void message(String acceptLanguage, String expectedMessage) { + RestAssured.given().when() + .header("Accept-Language", acceptLanguage) + .get("/message") + .then() + .statusCode(HttpStatus.SC_OK) + .body(is(expectedMessage)) + .log().all(); + } + + /** + * @see integration-tests/hibernate-validator/src/test/java/io/quarkus/it/hibernate/validator/HibernateValidatorFunctionalityTest.java + */ + @ParameterizedTest + @CsvSource(value = { + // Croatian language preference is higher than Ukrainian. + "en-US;q=0.25,hr-HR;q=0.9,fr-FR;q=0.5,uk-UA;q=0.1|Vrijednost ne zadovoljava uzorak", + // Ukrainian language preference is higher than Croatian. + "en-US;q=0.25,hr-HR;q=0.9,fr-FR;q=0.5,uk-UA;q=1.0|Значення не відповідає зразку", + // An unknown language preference, silent fallback to lingua franca. + "invalid string|Value is not in line with the pattern", + // Croatian language preference is the highest. + "en-US;q=0.25,hr-HR;q=1,fr-FR;q=0.5|Vrijednost ne zadovoljava uzorak", + // Chinese language preference is the highest. + "en-US;q=0.25,hr-HR;q=0.30,zh;q=0.9,fr-FR;q=0.50|數值不符合樣品", + }, 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/src/test/java/io/quarkus/locales/it/LocalesTest.java b/integration-tests/locales/all/src/test/java/io/quarkus/locales/it/LocalesTest.java similarity index 100% rename from integration-tests/locales/src/test/java/io/quarkus/locales/it/LocalesTest.java rename to integration-tests/locales/all/src/test/java/io/quarkus/locales/it/LocalesTest.java diff --git a/integration-tests/locales/all/src/test/resources/AppMessages_cs.properties b/integration-tests/locales/all/src/test/resources/AppMessages_cs.properties new file mode 100644 index 0000000000000..8ec64c41b1487 --- /dev/null +++ b/integration-tests/locales/all/src/test/resources/AppMessages_cs.properties @@ -0,0 +1 @@ +msg1=Ahoj světe! diff --git a/integration-tests/locales/all/src/test/resources/AppMessages_en.properties b/integration-tests/locales/all/src/test/resources/AppMessages_en.properties new file mode 100644 index 0000000000000..06c7c8f7add47 --- /dev/null +++ b/integration-tests/locales/all/src/test/resources/AppMessages_en.properties @@ -0,0 +1 @@ +msg1=Hello world! diff --git a/integration-tests/locales/all/src/test/resources/AppMessages_uk.properties b/integration-tests/locales/all/src/test/resources/AppMessages_uk.properties new file mode 100644 index 0000000000000..39b9ad30c6824 --- /dev/null +++ b/integration-tests/locales/all/src/test/resources/AppMessages_uk.properties @@ -0,0 +1 @@ +msg1=Привіт Світ! diff --git a/integration-tests/locales/all/src/test/resources/ValidationMessages.properties b/integration-tests/locales/all/src/test/resources/ValidationMessages.properties new file mode 100644 index 0000000000000..48a3bbf23dce1 --- /dev/null +++ b/integration-tests/locales/all/src/test/resources/ValidationMessages.properties @@ -0,0 +1 @@ +pattern.message=Value is not in line with the pattern diff --git a/integration-tests/locales/all/src/test/resources/ValidationMessages_hr_HR.properties b/integration-tests/locales/all/src/test/resources/ValidationMessages_hr_HR.properties new file mode 100644 index 0000000000000..ae2e444a98105 --- /dev/null +++ b/integration-tests/locales/all/src/test/resources/ValidationMessages_hr_HR.properties @@ -0,0 +1 @@ +pattern.message=Vrijednost ne zadovoljava uzorak diff --git a/integration-tests/locales/all/src/test/resources/ValidationMessages_uk_UA.properties b/integration-tests/locales/all/src/test/resources/ValidationMessages_uk_UA.properties new file mode 100644 index 0000000000000..0c37428808cfd --- /dev/null +++ b/integration-tests/locales/all/src/test/resources/ValidationMessages_uk_UA.properties @@ -0,0 +1 @@ +pattern.message=Значення не відповідає зразку diff --git a/integration-tests/locales/all/src/test/resources/ValidationMessages_zh.properties b/integration-tests/locales/all/src/test/resources/ValidationMessages_zh.properties new file mode 100644 index 0000000000000..0d169f05f8cd0 --- /dev/null +++ b/integration-tests/locales/all/src/test/resources/ValidationMessages_zh.properties @@ -0,0 +1 @@ +pattern.message=數值不符合樣品 diff --git a/integration-tests/locales/all/src/test/resources/application.properties b/integration-tests/locales/all/src/test/resources/application.properties new file mode 100644 index 0000000000000..acc6621c863f2 --- /dev/null +++ b/integration-tests/locales/all/src/test/resources/application.properties @@ -0,0 +1,2 @@ +quarkus.locales=all +quarkus.native.resources.includes=AppMessages_*.properties diff --git a/integration-tests/locales/app/pom.xml b/integration-tests/locales/app/pom.xml new file mode 100644 index 0000000000000..dae9bc473c177 --- /dev/null +++ b/integration-tests/locales/app/pom.xml @@ -0,0 +1,70 @@ + + + 4.0.0 + + io.quarkus + quarkus-integration-test-locales + 999-SNAPSHOT + + quarkus-integration-test-locales-app + Quarkus - Integration Tests - Locales - App + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-resteasy-reactive + + + io.quarkus + quarkus-hibernate-validator + + + + + io.quarkus + quarkus-arc-deployment + ${project.version} + pom + test + + + * + * + + + + + + io.quarkus + quarkus-resteasy-reactive-deployment + ${project.version} + pom + test + + + * + * + + + + + + io.quarkus + quarkus-hibernate-validator-deployment + ${project.version} + pom + test + + + * + * + + + + + diff --git a/integration-tests/locales/app/src/main/java/io/quarkus/locales/it/LocalesResource.java b/integration-tests/locales/app/src/main/java/io/quarkus/locales/it/LocalesResource.java new file mode 100644 index 0000000000000..630236903e87d --- /dev/null +++ b/integration-tests/locales/app/src/main/java/io/quarkus/locales/it/LocalesResource.java @@ -0,0 +1,78 @@ +package io.quarkus.locales.it; + +import java.text.NumberFormat; +import java.text.ParseException; +import java.time.ZoneId; +import java.util.Currency; +import java.util.Locale; +import java.util.ResourceBundle; +import java.util.TimeZone; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.Response; + +import org.jboss.logging.Logger; + +import io.smallrye.common.constraint.NotNull; + +@Path("") +public class LocalesResource { + + private static final Logger LOG = Logger.getLogger(LocalesResource.class); + + @Path("/locale/{country}/{language}") + @GET + public Response inLocale(@PathParam("country") String country, @NotNull @PathParam("language") String language) { + return Response.ok().entity(Locale.forLanguageTag(country).getDisplayCountry(new Locale(language))).build(); + } + + @Path("/default/{country}") + @GET + public Response inDefaultLocale(@PathParam("country") String country) { + return Response.ok().entity(Locale.forLanguageTag(country).getDisplayCountry()).build(); + } + + @Path("/currency/{country}/{language}") + @GET + public Response currencyInLocale(@PathParam("country") String country, @NotNull @PathParam("language") String language) { + return Response.ok().entity(Currency.getInstance(Locale.forLanguageTag(country)).getDisplayName(new Locale(language))) + .build(); + } + + @Path("/timeZone") + @GET + public Response timeZoneInLocale(@NotNull @QueryParam("zone") String zone, + @NotNull @QueryParam("language") String language) { + return Response.ok().entity(TimeZone.getTimeZone(ZoneId.of(zone)).getDisplayName(new Locale(language))).build(); + } + + @Path("/numbers") + @GET + public Response decimalDotCommaLocale(@NotNull @QueryParam("locale") String locale, + @NotNull @QueryParam("number") String number) throws ParseException { + final Locale l = Locale.forLanguageTag(locale); + LOG.infof("Locale: %s, Locale tag: %s, Number: %s", l, locale, number); + return Response.ok().entity(String.valueOf(NumberFormat.getInstance(l).parse(number).doubleValue())).build(); + } + + @Path("/ranges") + @GET + public Response ranges(@NotNull @QueryParam("range") String range) { + LOG.infof("Range: %s", range); + return Response.ok().entity(Locale.LanguageRange.parse(range).toString()).build(); + } + + @Path("/message") + @GET + public Response message(@Context HttpHeaders headers) { + final Locale locale = headers.getAcceptableLanguages().get(0); + LOG.infof("Locale: %s, language: %s, country: %s", locale, locale.getLanguage(), locale.getCountry()); + return Response.ok().entity(ResourceBundle.getBundle("AppMessages", locale).getString("msg1")).build(); + } + +} diff --git a/integration-tests/locales/pom.xml b/integration-tests/locales/pom.xml index d8d6704fdfcaf..378ad2423218d 100644 --- a/integration-tests/locales/pom.xml +++ b/integration-tests/locales/pom.xml @@ -10,87 +10,10 @@ quarkus-integration-test-locales Quarkus - Integration Tests - Locales - - - io.quarkus - quarkus-arc - - - io.quarkus - quarkus-resteasy - - - - - io.quarkus - quarkus-junit5 - test - - - io.rest-assured - rest-assured - test - - - - - io.quarkus - quarkus-arc-deployment - ${project.version} - pom - test - - - * - * - - - - - - io.quarkus - quarkus-resteasy-deployment - ${project.version} - pom - test - - - * - * - - - - - - - - - - src/test/resources - true - - - - - io.quarkus - quarkus-maven-plugin - - - - build - - - - - - maven-surefire-plugin - - 1 - false - - - - - + pom + + app + all + some + - diff --git a/integration-tests/locales/some/pom.xml b/integration-tests/locales/some/pom.xml new file mode 100644 index 0000000000000..7f75f67f190c9 --- /dev/null +++ b/integration-tests/locales/some/pom.xml @@ -0,0 +1,99 @@ + + + 4.0.0 + + io.quarkus + quarkus-integration-test-locales + 999-SNAPSHOT + + quarkus-integration-test-locales-some + Quarkus - Integration Tests - Locales - Some + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-resteasy-reactive + + + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + io.quarkus + quarkus-integration-test-locales-app + 999-SNAPSHOT + compile + + + + + io.quarkus + quarkus-arc-deployment + ${project.version} + pom + test + + + * + * + + + + + + io.quarkus + quarkus-resteasy-reactive-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/some/src/main/java/io/quarkus/locales/it/SomeLocalesResource.java b/integration-tests/locales/some/src/main/java/io/quarkus/locales/it/SomeLocalesResource.java new file mode 100644 index 0000000000000..b16ceeb45f58e --- /dev/null +++ b/integration-tests/locales/some/src/main/java/io/quarkus/locales/it/SomeLocalesResource.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 SomeLocalesResource extends LocalesResource { + private static final Logger LOG = Logger.getLogger(SomeLocalesResource.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/src/test/java/io/quarkus/locales/it/LocalesIT.java b/integration-tests/locales/some/src/test/java/io/quarkus/locales/it/LocalesIT.java similarity index 58% rename from integration-tests/locales/src/test/java/io/quarkus/locales/it/LocalesIT.java rename to integration-tests/locales/some/src/test/java/io/quarkus/locales/it/LocalesIT.java index 2c705f09c81c4..4b192ba4ca03a 100644 --- a/integration-tests/locales/src/test/java/io/quarkus/locales/it/LocalesIT.java +++ b/integration-tests/locales/some/src/test/java/io/quarkus/locales/it/LocalesIT.java @@ -1,5 +1,6 @@ package io.quarkus.locales.it; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalToIgnoringCase; import static org.hamcrest.Matchers.is; @@ -7,19 +8,16 @@ import org.jboss.logging.Logger; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; +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 - * + * 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 { @@ -27,61 +25,58 @@ public class LocalesIT { private static final Logger LOG = Logger.getLogger(LocalesIT.class); @ParameterizedTest - @ValueSource(strings = { + @CsvSource(value = { "en-US|en|United States", "de-DE|de|Deutschland", "de-AT|en|Austria", "de-DE|en|Germany" - }) - public void testCorrectLocales(String countryLanguageTranslation) { - final String[] lct = countryLanguageTranslation.split("\\|"); - LOG.infof("Triggering test: Country: %s, Language: %s, Translation: %s", lct[0], lct[1], lct[2]); + }, delimiter = '|') + public void testCorrectLocales(String country, String language, String translation) { + LOG.infof("Triggering test: Country: %s, Language: %s, Translation: %s", country, language, translation); RestAssured.given().when() - .get(String.format("/locale/%s/%s", lct[0], lct[1])) + .get(String.format("/locale/%s/%s", country, language)) .then() .statusCode(HttpStatus.SC_OK) - .body(is(lct[2])) + .body(is(translation)) .log().all(); } @ParameterizedTest - @ValueSource(strings = { + @CsvSource(value = { "en-US|en|US Dollar", "de-DE|fr|euro", "cs-CZ|cs|česká koruna", "ja-JP|ja|日本円", "en-TZ|en|Tanzanian Shilling", "uk-UA|uk|українська гривня" - }) - public void testCurrencies(String countryLanguageCurrency) { - final String[] clc = countryLanguageCurrency.split("\\|"); - LOG.infof("Triggering test: Country: %s, Language: %s, Currency: %s", clc[0], clc[1], clc[2]); + }, delimiter = '|') + public void testCurrencies(String country, String language, String currency) { + LOG.infof("Triggering test: Country: %s, Language: %s, Currency: %s", country, language, currency); RestAssured.given().when() - .get(String.format("/currency/%s/%s", clc[0], clc[1])) + .get(String.format("/currency/%s/%s", country, language)) .then() .statusCode(HttpStatus.SC_OK) - .body(is(clc[2])) + .body(is(currency)) .log().all(); } @ParameterizedTest - @ValueSource(strings = { + @CsvSource(value = { "Asia/Tokyo|fr|heure normale du Japon", "Europe/Prague|cs|Středoevropský standardní čas", "GMT|fr|heure moyenne de Greenwich", "Asia/Yerevan|ja|アルメニア標準時", "US/Pacific|uk|за північноамериканським тихоокеанським стандартним часом" - }) - public void testTimeZones(String zoneLanguageName) { - final String[] zln = zoneLanguageName.split("\\|"); - LOG.infof("Triggering test: Zone: %s, Language: %s, Name: %s", zln[0], zln[1], zln[2]); + }, delimiter = '|') + public void testTimeZones(String zone, String language, String name) { + LOG.infof("Triggering test: Zone: %s, Language: %s, Name: %s", zone, language, name); RestAssured.given().when() - .param("zone", zln[0]) - .param("language", zln[1]) + .param("zone", zone) + .param("language", language) .get("/timeZone") .then() .statusCode(HttpStatus.SC_OK) - .body(equalToIgnoringCase(zln[2])) + .body(equalToIgnoringCase(name)) .log().all(); } @@ -113,4 +108,27 @@ public void testMissingLocaleSorryItaly() { .body(is("Italy")) .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/some/src/test/java/io/quarkus/locales/it/LocalesTest.java b/integration-tests/locales/some/src/test/java/io/quarkus/locales/it/LocalesTest.java new file mode 100644 index 0000000000000..bd42cd4ae7fe7 --- /dev/null +++ b/integration-tests/locales/some/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/some/src/test/resources/ValidationMessages.properties b/integration-tests/locales/some/src/test/resources/ValidationMessages.properties new file mode 100644 index 0000000000000..48a3bbf23dce1 --- /dev/null +++ b/integration-tests/locales/some/src/test/resources/ValidationMessages.properties @@ -0,0 +1 @@ +pattern.message=Value is not in line with the pattern diff --git a/integration-tests/locales/some/src/test/resources/ValidationMessages_fr_FR.properties b/integration-tests/locales/some/src/test/resources/ValidationMessages_fr_FR.properties new file mode 100644 index 0000000000000..c9a6e5d71d6f0 --- /dev/null +++ b/integration-tests/locales/some/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/some/src/test/resources/ValidationMessages_hr_HR.properties b/integration-tests/locales/some/src/test/resources/ValidationMessages_hr_HR.properties new file mode 100644 index 0000000000000..ae2e444a98105 --- /dev/null +++ b/integration-tests/locales/some/src/test/resources/ValidationMessages_hr_HR.properties @@ -0,0 +1 @@ +pattern.message=Vrijednost ne zadovoljava uzorak diff --git a/integration-tests/locales/src/test/resources/application.properties b/integration-tests/locales/some/src/test/resources/application.properties similarity index 87% rename from integration-tests/locales/src/test/resources/application.properties rename to integration-tests/locales/some/src/test/resources/application.properties index 21474320198a3..4d3a429e5788c 100644 --- a/integration-tests/locales/src/test/resources/application.properties +++ b/integration-tests/locales/some/src/test/resources/application.properties @@ -1,4 +1,4 @@ -quarkus.locales=de,fr,ja,uk-UA +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.native.user-language=cs diff --git a/integration-tests/locales/src/main/java/io/quarkus/locales/it/LocalesResource.java b/integration-tests/locales/src/main/java/io/quarkus/locales/it/LocalesResource.java deleted file mode 100644 index ad2ff844decb3..0000000000000 --- a/integration-tests/locales/src/main/java/io/quarkus/locales/it/LocalesResource.java +++ /dev/null @@ -1,46 +0,0 @@ -package io.quarkus.locales.it; - -import java.time.ZoneId; -import java.util.Currency; -import java.util.Locale; -import java.util.TimeZone; - -import jakarta.validation.constraints.NotNull; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.core.Response; - -@Path("") -public class LocalesResource { - - @Path("/locale/{country}/{language}") - @GET - public Response inLocale(@PathParam("country") String country, @NotNull @PathParam("language") String language) { - return Response.ok().entity( - Locale.forLanguageTag(country).getDisplayCountry(new Locale(language))).build(); - } - - @Path("/default/{country}") - @GET - public Response inDefaultLocale(@PathParam("country") String country) { - return Response.ok().entity( - Locale.forLanguageTag(country).getDisplayCountry()).build(); - } - - @Path("/currency/{country}/{language}") - @GET - public Response currencyInLocale(@PathParam("country") String country, @NotNull @PathParam("language") String language) { - return Response.ok().entity( - Currency.getInstance(Locale.forLanguageTag(country)).getDisplayName(new Locale(language))).build(); - } - - @Path("/timeZone") - @GET - public Response timeZoneInLocale(@NotNull @QueryParam("zone") String zone, - @NotNull @QueryParam("language") String language) { - return Response.ok().entity( - TimeZone.getTimeZone(ZoneId.of(zone)).getDisplayName(new Locale(language))).build(); - } -}