Skip to content

Commit

Permalink
Introduces quarkus.locales=all
Browse files Browse the repository at this point in the history
  • Loading branch information
Karm committed Nov 15, 2023
1 parent 7f50299 commit b5e3748
Show file tree
Hide file tree
Showing 22 changed files with 472 additions and 88 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -731,7 +731,11 @@ public NativeImageInvokerInfo build() {
}
final String includeLocales = LocaleProcessor.nativeImageIncludeLocales(nativeConfig, localesBuildTimeConfig);
if (!includeLocales.isEmpty()) {
addExperimentalVMOption(nativeImageArgs, "-H:IncludeLocales=" + includeLocales);
if ("all".equals(includeLocales)) {
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
2 changes: 2 additions & 0 deletions docs/src/main/asciidoc/validation.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,8 @@ 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 inflates the executable size though.

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
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,7 @@
package io.quarkus.locales.it;

import jakarta.ws.rs.Path;

@Path("")
public class AllLocalesResource extends LocalesResource {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package io.quarkus.locales.it;

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.ValueSource;

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
@ValueSource(strings = {
"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"
})
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]);
RestAssured.given().when()
.get(String.format("/locale/%s/%s", lct[0], lct[1]))
.then()
.statusCode(HttpStatus.SC_OK)
.body(is(lct[2]))
.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

@ValueSource(strings = {
"0,666|en-US|666.0",
"0,666|cs-CZ|0.666",
"0,666|fr-FR|0.666",
"0.666|fr-FR|0.0"
})
public void testNumbers(String zoneLanguageName) {
final String[] nlr = zoneLanguageName.split("\\|");
LOG.infof("Triggering test: Number: %s, Locale: %s, Expected result: %s", nlr[0], nlr[1], nlr[2]);
RestAssured.given().when()
.param("number", nlr[0])
.param("locale", nlr[1])
.get("/numbers")
.then()
.statusCode(HttpStatus.SC_OK)
.body(equalTo(nlr[2]))
.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();
}

@Test
public void message() {
// Ukrainian language preference is higher than Czech.
RestAssured.given().when()
.header("Accept-Language", "cs;q=0.7,uk;q=0.9")
.get("/message")
.then()
.statusCode(HttpStatus.SC_OK)
.body(is("Привіт Світ!"))
.log().all();
// Czech language preference is higher than Ukrainian.
RestAssured.given().when()
.header("Accept-Language", "cs;q=1.0,uk;q=0.9")
.get("/message")
.then()
.statusCode(HttpStatus.SC_OK)
.body(is("Ahoj světe!"))
.log().all();
// An unknown language preference, silent fallback to lingua franca.
RestAssured.given().when()
.header("Accept-Language", "jp;q=1.0")
.get("/message")
.then()
.statusCode(HttpStatus.SC_OK)
.body(is("Hello world!"))
.log().all();
}
}
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,2 @@
quarkus.locales=all
quarkus.native.resources.includes=AppMessages_*.properties
52 changes: 52 additions & 0 deletions integration-tests/locales/app/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?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-app</artifactId>
<name>Quarkus - Integration Tests - Locales - App</name>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive</artifactId>
</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>
</project>
Loading

0 comments on commit b5e3748

Please sign in to comment.