Skip to content

Commit

Permalink
Add internationalization support
Browse files Browse the repository at this point in the history
  • Loading branch information
jpraet committed Nov 3, 2024
1 parent 2e472da commit b606b1a
Show file tree
Hide file tree
Showing 31 changed files with 659 additions and 56 deletions.
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 @@ -134,6 +134,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 @@ -331,4 +332,39 @@ void constraintViolationBodyInheritance() {
.body("issues[1].detail", equalTo("must not be blank"));
}

@Test
void i18n() {
getSpec().when()
.header("Accept-Language", "nl-BE")
.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 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"));
}

}
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
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 Down Expand Up @@ -92,6 +97,17 @@
</execution>
</executions>
</plugin>
<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>
</plugins>
</build>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package io.github.belgif.rest.problem.jaxrs;

import javax.annotation.PostConstruct;
import javax.servlet.ServletContext;
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.core.Context;
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 I18N helper (and clears it afterward).
*/
@PreMatching
@Provider
public class I18NAcceptLanguageFilter implements ContainerRequestFilter, ContainerResponseFilter {

public static final String I18N_FLAG = "io.github.belgif.rest.problem.i18n";

@Context
private ServletContext servletContext;

private boolean enabled = true;

@PostConstruct
public void initialize() {
if (servletContext.getInitParameter(I18N_FLAG) != null) {
enabled = Boolean.parseBoolean(servletContext.getInitParameter(I18N_FLAG));
} else if (System.getProperty(I18N_FLAG) != null) {
enabled = Boolean.parseBoolean(System.getProperty(I18N_FLAG));
} else if (System.getenv(I18N_FLAG) != null) {
enabled = Boolean.parseBoolean(System.getenv(I18N_FLAG));
} else {
enabled = true;
}
}

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

@Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) {
if (enabled) {
I18N.clearRequestLocale();
}
}

protected void setServletContext(ServletContext servletContext) {
this.servletContext = servletContext;
}

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

import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;

import java.util.Collections;
import java.util.Locale;

import javax.servlet.ServletContext;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.SetEnvironmentVariable;
import org.junitpioneer.jupiter.SetSystemProperty;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

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

@ExtendWith(MockitoExtension.class)
class I18NAcceptLanguageFilterTest {

@InjectMocks
private I18NAcceptLanguageFilter filter;

@Mock
private ContainerRequestContext requestContext;

@Mock
private ContainerResponseContext responseContext;

@Mock
private ServletContext servletContext;

@AfterEach
void cleanup() {
I18N.clearRequestLocale();
}

@Test
void languageRequested() {
when(requestContext.getAcceptableLanguages()).thenReturn(Collections.singletonList(new Locale("nl", "BE")));
filter.filter(requestContext);
assertThat(I18N.getRequestLocale()).isEqualTo(new Locale("nl", "BE"));
}

@Test
void noLanguageRequested() {
when(requestContext.getAcceptableLanguages()).thenReturn(Collections.emptyList());
filter.filter(requestContext);
assertThat(I18N.getRequestLocale()).isEqualTo(new Locale("en"));
}

@Test
void clearLocaleAfterResponse() {
I18N.setRequestLocale(new Locale("nl", "BE"));
filter.filter(requestContext, responseContext);
assertThat(I18N.getRequestLocale()).isEqualTo(new Locale("en"));
}

@Test
void disabledViaInitParam() {
when(servletContext.getInitParameter(I18NAcceptLanguageFilter.I18N_FLAG)).thenReturn("false");
filter.initialize();
filter.filter(requestContext);
verifyNoInteractions(requestContext);
}

@Test
@SetSystemProperty(key = I18NAcceptLanguageFilter.I18N_FLAG, value = "false")
void disabledViaSystemProperty() {
when(servletContext.getInitParameter(I18NAcceptLanguageFilter.I18N_FLAG)).thenReturn(null);
filter.initialize();
filter.filter(requestContext);
verifyNoInteractions(requestContext);
}

@Test
@SetEnvironmentVariable(key = I18NAcceptLanguageFilter.I18N_FLAG, value = "false")
void disabledViaEnvironmentVariable() {
when(servletContext.getInitParameter(I18NAcceptLanguageFilter.I18N_FLAG)).thenReturn(null);
filter.initialize();
filter.filter(requestContext);
verifyNoInteractions(requestContext);
}

}
5 changes: 5 additions & 0 deletions belgif-rest-problem-spring/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@
<artifactId>spring-web</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package io.github.belgif.rest.problem.spring;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.support.RequestContextUtils;

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

/**
* Filter that registers the requested locale, as specified in Accept-Language HTTP header,
* with the I18N helper (and clears it afterward).
*/
@Component
@ConditionalOnProperty(prefix = "io.github.belgif.rest.problem", name = "i18n", havingValue = "true",
matchIfMissing = true)
public class I18NAcceptLanguageFilter implements Filter {

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
I18N.setRequestLocale(RequestContextUtils.getLocale((HttpServletRequest) servletRequest));
try {
filterChain.doFilter(servletRequest, servletResponse);
} finally {
I18N.clearRequestLocale();
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ public class ProblemConfigurationProperties {

private List<String> scanAdditionalProblemPackages = new ArrayList<>();

private boolean i18n = true;

public void setScanAdditionalProblemPackages(List<String> scanAdditionalProblemPackages) {
this.scanAdditionalProblemPackages = scanAdditionalProblemPackages;
}
Expand All @@ -21,4 +23,12 @@ public List<String> getScanAdditionalProblemPackages() {
return scanAdditionalProblemPackages;
}

public void setI18n(boolean i18n) {
this.i18n = i18n;
}

public boolean isI18n() {
return i18n;
}

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

import static org.assertj.core.api.Assertions.*;

import java.io.IOException;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicReference;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;

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

@ExtendWith(MockitoExtension.class)
class I18NAcceptLanguageFilterTest {

private final I18NAcceptLanguageFilter filter = new I18NAcceptLanguageFilter();

@Mock
private FilterChain filterChain;

@Test
void languageRequested() throws ServletException, IOException {
MockHttpServletRequest servletRequest = new MockHttpServletRequest();
MockHttpServletResponse servletResponse = new MockHttpServletResponse();
servletRequest.addHeader("Accept-Language", "nl-BE");
AtomicReference<Locale> locale = new AtomicReference<>();
Mockito.doAnswer(invocationOnMock -> {
locale.set(I18N.getRequestLocale());
return null;
}).when(filterChain).doFilter(servletRequest, servletResponse);
filter.doFilter(servletRequest, servletResponse, filterChain);
// during the request processing, locale should be nl-BE
assertThat(locale).hasValue(new Locale("nl", "BE"));
// after the request processing, locale should be restored to default = en
assertThat(I18N.getRequestLocale()).isEqualTo(new Locale("en"));
}

@Test
void noLanguageRequested() throws ServletException, IOException {
MockHttpServletRequest servletRequest = new MockHttpServletRequest();
MockHttpServletResponse servletResponse = new MockHttpServletResponse();
AtomicReference<Locale> locale = new AtomicReference<>();
Mockito.doAnswer(invocationOnMock -> {
locale.set(I18N.getRequestLocale());
return null;
}).when(filterChain).doFilter(servletRequest, servletResponse);
filter.doFilter(servletRequest, servletResponse, filterChain);
// during the request processing, locale should be default = en
assertThat(locale).hasValue(new Locale("en"));
// after the request processing, locale should be restored to default = en
assertThat(I18N.getRequestLocale()).isEqualTo(new Locale("en"));
}

}
Loading

0 comments on commit b606b1a

Please sign in to comment.