Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add internationalization support #133

Open
wants to merge 32 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
b606b1a
Add internationalization support
jpraet Nov 3, 2024
4b3a1c1
Merge branch 'main' into feature/#124-i18n
jpraet Nov 5, 2024
d4d0770
Add french and german translations (by CBSS translation service)
jpraet Nov 5, 2024
f585c5c
Pluggable LocaleResolver
jpraet Nov 5, 2024
73997eb
Remove obsolete dependency
jpraet Nov 5, 2024
a683b8d
Locale constructor is deprecated in JDK21
jpraet Nov 5, 2024
075d8f5
Sonar
jpraet Nov 5, 2024
ec89168
Merge branch 'main' into feature/#124-i18n
jpraet Nov 6, 2024
4508118
Merge branch 'main' into feature/#124-i18n
jpraet Nov 19, 2024
07b96a1
Merge branch 'main' into feature/#124-i18n
jpraet Nov 28, 2024
84fcb31
Correct french translations
jflabatBCSS Nov 29, 2024
1a091fa
Merge remote-tracking branch 'origin/feature/#124-i18n' into feature/…
jflabatBCSS Nov 29, 2024
170d04a
handle review
jpraet Nov 29, 2024
66412bd
Fix french translations after review of @usku01
jflabatBCSS Nov 29, 2024
a8103be
Merge branch 'main' into feature/#124-i18n
jpraet Dec 6, 2024
d0d46b4
Minor message tweaks
jpraet Dec 8, 2024
28ab016
Merge branch 'main' into feature/#124-i18n
jpraet Dec 9, 2024
6028ec9
Fix translation of zeroOrExactlyOneOfExpected.detail
jflabatBCSS Dec 10, 2024
58c298f
Merge branch 'main' into feature/#124-i18n
jpraet Dec 11, 2024
0bee5cc
Add check from missing keys in I18N bundle
jpraet Dec 14, 2024
238ceb7
Use Locale.ENGLISH
jpraet Dec 14, 2024
2a483e1
Extract I18NConfigurator from I18NAcceptLanguageFilter
jpraet Dec 14, 2024
f2a0a56
Disable I18N tests on Quarkus (not working)
jpraet Dec 14, 2024
7d53e5c
Add i18nDisabled test
jpraet Dec 14, 2024
5cd75d6
Move system property / environment property configuration to belgif-r…
jpraet Dec 14, 2024
3cd8f4c
Add test for weighted Accept-Language header
jpraet Dec 14, 2024
c77f154
Document LocaleResolver
jpraet Dec 15, 2024
7587a79
Support I18N on Quarkus
jpraet Dec 16, 2024
cf9795a
Add integration tests for unsupported language
jpraet Dec 16, 2024
8367304
Add integration test for disabled I18N
jpraet Dec 16, 2024
a7e75e4
Add InputValidationIssue.localizedDetail()
jpraet Dec 16, 2024
16543b8
Merge branch 'main' into feature/#124-i18n
jpraet Dec 17, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import io.github.belgif.rest.problem.api.ClientProblem;
import io.github.belgif.rest.problem.api.ProblemType;
import io.github.belgif.rest.problem.i18n.I18N;

@ProblemType(CustomProblem.TYPE)
public class CustomProblem extends ClientProblem {
Expand All @@ -24,6 +25,7 @@ public class CustomProblem extends ClientProblem {
@JsonCreator
public CustomProblem(@JsonProperty("customField") String customField) {
super(TYPE_URI, TITLE, STATUS);
setDetail(I18N.getLocalizedDetail(CustomProblem.class));
this.customField = customField;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ void constraintViolationMissingRequiredQueryParameter() {
.get("/beanValidation/queryParameter").then().assertThat()
.statusCode(400)
.body("type", equalTo("urn:problem-type:belgif:badRequest"))
.body("detail", equalTo("The input message is incorrect"))
.body("issues[0].type", equalTo("urn:problem-type:belgif:input-validation:schemaViolation"))
.body("issues[0].title", equalTo("Input value is invalid with respect to the schema"))
.body("issues[0].in", equalTo("query"))
Expand Down Expand Up @@ -338,4 +339,90 @@ void constraintViolationBodyInheritance() {
.body("issues[1].detail", equalTo("must not be blank"));
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We never test localization can be disabled, would it be easy to add a test for that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we could expose an endpoint that calls I18N.setEnabled(false).

@Test
void i18n() {
getSpec().when()
.header("Accept-Language", "nl-BE")
Copy link
Collaborator

@clone1612 clone1612 Dec 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be interesting to add an additional test that verifies behavior with multiple values that use weighting

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test added. Note that we just take the one with the highest weight though, and if that one is not supported, it will fallback to English. So with Accept-Language = "fr-BE;q=0.5, es;q=0.7" the resulting language will be English, not French.

.queryParam("param", -1)
.queryParam("other", "TOO_LONG")
.get("/beanValidation/queryParameter").then().assertThat()
.statusCode(400)
.body("type", equalTo("urn:problem-type:belgif:badRequest"))
.body("detail", equalTo("Het input bericht is ongeldig"))
.body("issues[0].type", equalTo("urn:problem-type:belgif:input-validation:schemaViolation"))
.body("issues[0].title", equalTo("Input value is invalid with respect to the schema"))
.body("issues[0].detail", equalTo("grootte moet tussen 0 en 5 liggen"))
.body("issues[0].in", equalTo("query"))
.body("issues[0].name", equalTo("other"))
.body("issues[0].value", equalTo("TOO_LONG"))
.body("issues[1].type", equalTo("urn:problem-type:belgif:input-validation:schemaViolation"))
.body("issues[1].title", equalTo("Input value is invalid with respect to the schema"))
.body("issues[1].detail", equalTo("moet groter dan 0 zijn"))
.body("issues[1].in", equalTo("query"))
.body("issues[1].name", equalTo("param"))
.body("issues[1].value", equalTo(-1));
}

@Test
void i18nUnsupportedLanguage() {
getSpec().when()
.header("Accept-Language", "es")
.queryParam("param", -1)
.queryParam("other", "TOO_LONG")
.get("/beanValidation/queryParameter").then().assertThat()
.statusCode(400)
.body("detail", equalTo("The input message is incorrect"));
}

@Test
void i18nWeighted() {
getSpec().when()
.header("Accept-Language", "fr-BE;q=0.5, nl-BE;q=0.7")
.queryParam("param", -1)
.queryParam("other", "TOO_LONG")
.get("/beanValidation/queryParameter").then().assertThat()
.statusCode(400)
.body("type", equalTo("urn:problem-type:belgif:badRequest"))
.body("detail", equalTo("Het input bericht is ongeldig"))
.body("issues[0].detail", equalTo("grootte moet tussen 0 en 5 liggen"));
}

@Test
void i18nUnsupportedLanguageWeighted() {
getSpec().when()
.header("Accept-Language", "fr-BE;q=0.5, es;q=0.7")
.queryParam("param", -1)
.queryParam("other", "TOO_LONG")
.get("/beanValidation/queryParameter").then().assertThat()
.statusCode(400)
.body("detail", equalTo("The input message is incorrect"));
}

@Test
void i18nCustom() {
getSpec().when()
.header("Accept-Language", "nl-BE")
.get("/custom").then().assertThat()
.statusCode(409)
.body("type", equalTo("urn:problem-type:acme:custom"))
.body("customField", equalTo("value from frontend"))
.body("detail", equalTo("NL detail"));
}

@Test
void i18nDisabled() {
try {
getSpec().queryParam("enabled", "false").post("/i18n");
getSpec().when()
.header("Accept-Language", "nl-BE")
.queryParam("param", -1)
.queryParam("other", "TOO_LONG")
.get("/beanValidation/queryParameter").then().assertThat()
.statusCode(400)
.body("detail", equalTo("The input message is incorrect"));
} finally {
getSpec().queryParam("enabled", "true").post("/i18n");
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CustomProblem.detail=Custom detail
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CustomProblem.detail=DE detail
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CustomProblem.detail=FR detail
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CustomProblem.detail=NL detail
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.QueryParam;
Expand All @@ -20,6 +21,7 @@
import com.acme.custom.CustomProblem;

import io.github.belgif.rest.problem.api.Problem;
import io.github.belgif.rest.problem.i18n.I18N;
import io.github.belgif.rest.problem.jaxrs.client.ProblemSupport;
import io.github.belgif.rest.problem.model.ChildModel;
import io.github.belgif.rest.problem.model.Model;
Expand Down Expand Up @@ -248,4 +250,11 @@ public Response beanValidationBodyInheritance(ChildModel body) {
return Response.ok("body: " + body).build();
}

@POST
@Path("/i18n")
public Response i18n(@QueryParam("enabled") boolean enabled) {
I18N.setEnabled(enabled);
return Response.ok().build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
Expand All @@ -20,6 +21,7 @@
import com.acme.custom.CustomProblem;

import io.github.belgif.rest.problem.api.Problem;
import io.github.belgif.rest.problem.i18n.I18N;
import io.github.belgif.rest.problem.jaxrs.client.ProblemSupport;
import io.github.belgif.rest.problem.model.ChildModel;
import io.github.belgif.rest.problem.model.Model;
Expand Down Expand Up @@ -246,4 +248,12 @@ public Response beanValidationBodyNested(NestedModel body) {
public Response beanValidationBodyInheritance(ChildModel body) {
return Response.ok("body: " + body).build();
}

@POST
@Path("/i18n")
public Response i18n(@QueryParam("enabled") boolean enabled) {
I18N.setEnabled(enabled);
return Response.ok().build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.QueryParam;
Expand All @@ -24,6 +25,7 @@
import io.github.belgif.rest.problem.DefaultProblem;
import io.github.belgif.rest.problem.ServiceUnavailableProblem;
import io.github.belgif.rest.problem.api.Problem;
import io.github.belgif.rest.problem.i18n.I18N;
import io.github.belgif.rest.problem.model.ChildModel;
import io.github.belgif.rest.problem.model.Model;
import io.github.belgif.rest.problem.model.NestedModel;
Expand Down Expand Up @@ -255,4 +257,11 @@ public Response beanValidationBodyInheritance(ChildModel body) {
return Response.ok("body: " + body).build();
}

@POST
@Path("/i18n")
public Response i18n(@QueryParam("enabled") boolean enabled) {
I18N.setEnabled(enabled);
return Response.ok().build();
}

}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
quarkus.http.root-path=/quarkus
quarkus.locales=en,fr-BE,nl-BE,de-BE
quarkus.rest-client.backend.uri=http://localhost:${quarkus.http.port}/quarkus
%test.quarkus.rest-client.backend.uri=http://localhost:${quarkus.http.test-port}/quarkus
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import com.acme.custom.CustomProblem;

import io.github.belgif.rest.problem.api.Problem;
import io.github.belgif.rest.problem.i18n.I18N;
import io.github.belgif.rest.problem.model.ChildModel;
import io.github.belgif.rest.problem.model.Model;
import io.github.belgif.rest.problem.model.NestedModel;
Expand Down Expand Up @@ -203,4 +204,10 @@ public ResponseEntity<String> beanValidationQueryParameterNested(@Valid Model p)
return ResponseEntity.ok("param: " + p);
}

@PostMapping("/i18n")
public ResponseEntity<Void> i18n(@RequestParam("enabled") boolean enabled) {
I18N.setEnabled(enabled);
return ResponseEntity.ok().build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import com.acme.custom.CustomProblem;

import io.github.belgif.rest.problem.api.Problem;
import io.github.belgif.rest.problem.i18n.I18N;
import io.github.belgif.rest.problem.model.ChildModel;
import io.github.belgif.rest.problem.model.Model;
import io.github.belgif.rest.problem.model.NestedModel;
Expand Down Expand Up @@ -223,4 +224,10 @@ public ResponseEntity<String> beanValidationQueryParameterNested(@Valid Model p)
return ResponseEntity.ok("param: " + p);
}

@PostMapping("/i18n")
public ResponseEntity<Void> i18n(@RequestParam("enabled") boolean enabled) {
I18N.setEnabled(enabled);
return ResponseEntity.ok().build();
}

}
16 changes: 16 additions & 0 deletions belgif-rest-problem-java-ee/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit-pioneer</groupId>
<artifactId>junit-pioneer</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
Expand All @@ -79,6 +84,17 @@

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<!-- https://junit-pioneer.org/docs/environment-variables/#warnings-for-reflective-access -->
<argLine>
--add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.base/java.lang=ALL-UNNAMED
</argLine>
</configuration>
</plugin>
<plugin>
<groupId>org.eclipse.transformer</groupId>
<artifactId>transformer-maven-plugin</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.github.belgif.rest.problem.jaxrs.i18n;

import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.container.PreMatching;
import javax.ws.rs.ext.Provider;

import io.github.belgif.rest.problem.i18n.I18N;

/**
* Filter that registers the requested locale, as specified in Accept-Language HTTP header,
* with the {@link ThreadLocalLocaleResolver} (and clears it afterward).
*/
@PreMatching
@Provider
public class I18NAcceptLanguageFilter implements ContainerRequestFilter, ContainerResponseFilter {

@Override
public void filter(ContainerRequestContext requestContext) {
if (I18N.isEnabled() && !requestContext.getAcceptableLanguages().isEmpty()) {
ThreadLocalLocaleResolver.setLocale(requestContext.getAcceptableLanguages().get(0));
}
}

@Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) {
if (I18N.isEnabled()) {
ThreadLocalLocaleResolver.clear();
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.github.belgif.rest.problem.jaxrs.i18n;

import static io.github.belgif.rest.problem.i18n.I18N.*;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;

import io.github.belgif.rest.problem.i18n.I18N;

/**
* ServletContextListener that enables/disables I18N based on the "io.github.belgif.rest.problem.i18n"
* parameter, which is resolved from following configuration locations in order:
* <ol>
* <li>System property</li>
* <li>Environment variable</li>
* <li>web.xml context param</li>
* </ol>
*/
@WebListener
public class I18NConfigurator implements ServletContextListener {

@Override
public void contextInitialized(ServletContextEvent sce) {
if (System.getProperty(I18N_FLAG) != null) {
I18N.setEnabled(Boolean.parseBoolean(System.getProperty(I18N_FLAG)));
} else if (System.getenv(I18N_FLAG) != null) {
I18N.setEnabled(Boolean.parseBoolean(System.getenv(I18N_FLAG)));
} else if (sce.getServletContext().getInitParameter(I18N_FLAG) != null) {
I18N.setEnabled(Boolean.parseBoolean(sce.getServletContext().getInitParameter(I18N_FLAG)));
}
}

}
Loading
Loading