Skip to content

Commit

Permalink
Introduces quarkus.locales=all
Browse files Browse the repository at this point in the history
Hibernate validator interprets Locale.ROOT as array of all

Fixes hibernate-validator for quarkus.locales=all
  • Loading branch information
Karm committed Nov 28, 2023
1 parent f0c48e1 commit c025a1b
Show file tree
Hide file tree
Showing 31 changed files with 649 additions and 167 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Locale> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ public class LocalesBuildTimeConfig {
* <p>
* Native-image build uses it to define additional locales that are supposed
* to be available at runtime.
* <p>
* 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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions docs/src/main/asciidoc/validation.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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());

Expand Down
16 changes: 16 additions & 0 deletions integration-tests/locales/README.md
Original file line number Diff line number Diff line change
@@ -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.
100 changes: 100 additions & 0 deletions integration-tests/locales/all/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-integration-test-locales</artifactId>
<version>999-SNAPSHOT</version>
</parent>
<artifactId>quarkus-integration-test-locales-all</artifactId>
<name>Quarkus - Integration Tests - Locales - All</name>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive</artifactId>
</dependency>

<!-- test dependencies -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-integration-test-locales-app</artifactId>
<version>999-SNAPSHOT</version>
<scope>compile</scope>
</dependency>

<!-- Minimal test dependencies to *-deployment artifacts for consistent build order -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc-deployment</artifactId>
<version>${project.version}</version>
<type>pom</type>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive-deployment</artifactId>
<version>${project.version}</version>
<type>pom</type>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>

</dependencies>

<build>
<resources>
<resource>
<directory>src/test/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>build</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<forkCount>1</forkCount>
<reuseForks>false</reuseForks>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
msg1=Ahoj světe!
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
msg1=Hello world!
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
msg1=Привіт Світ!
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pattern.message=Value is not in line with the pattern
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pattern.message=Vrijednost ne zadovoljava uzorak
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pattern.message=Значення не відповідає зразку
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pattern.message=數值不符合樣品
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
quarkus.locales=all
quarkus.native.resources.includes=AppMessages_*.properties
Loading

0 comments on commit c025a1b

Please sign in to comment.