-
Notifications
You must be signed in to change notification settings - Fork 0
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
base: main
Are you sure you want to change the base?
Changes from 7 commits
b606b1a
4b3a1c1
d4d0770
f585c5c
73997eb
a683b8d
075d8f5
ec89168
4508118
07b96a1
84fcb31
1a091fa
170d04a
66412bd
a8103be
d0d46b4
28ab016
6028ec9
58c298f
0bee5cc
238ceb7
2a483e1
f2a0a56
7d53e5c
5cd75d6
3cd8f4c
c77f154
7587a79
cf9795a
8367304
a7e75e4
16543b8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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")) | ||
|
@@ -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") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
package io.github.belgif.rest.problem.jaxrs.i18n; | ||
|
||
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 {@link ThreadLocalLocaleResolver} (and clears it afterward). | ||
*/ | ||
@PreMatching | ||
@Provider | ||
public class I18NAcceptLanguageFilter implements ContainerRequestFilter, ContainerResponseFilter { | ||
|
||
@Context | ||
private ServletContext servletContext; | ||
|
||
@PostConstruct | ||
public void initialize() { | ||
if (servletContext.getInitParameter(I18N.I18N_FLAG) != null) { | ||
jpraet marked this conversation as resolved.
Show resolved
Hide resolved
|
||
I18N.setEnabled(Boolean.parseBoolean(servletContext.getInitParameter(I18N.I18N_FLAG))); | ||
} | ||
} | ||
|
||
@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(); | ||
} | ||
} | ||
|
||
protected void setServletContext(ServletContext servletContext) { | ||
jpraet marked this conversation as resolved.
Show resolved
Hide resolved
|
||
this.servletContext = servletContext; | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
package io.github.belgif.rest.problem.jaxrs.i18n; | ||
|
||
import java.util.Locale; | ||
|
||
import io.github.belgif.rest.problem.i18n.I18N; | ||
import io.github.belgif.rest.problem.i18n.LocaleResolver; | ||
|
||
/** | ||
* LocaleResolver implementation that uses a ThreadLocal. | ||
*/ | ||
public class ThreadLocalLocaleResolver implements LocaleResolver { | ||
|
||
/** | ||
* ThreadLocal with the locale of the current request (defaults to English). | ||
*/ | ||
private static final ThreadLocal<Locale> LOCALE = new InheritableThreadLocal<Locale>() { | ||
@Override | ||
protected Locale initialValue() { | ||
return I18N.DEFAULT_LOCALE; | ||
} | ||
}; | ||
|
||
@Override | ||
public Locale getLocale() { | ||
return LOCALE.get(); | ||
} | ||
|
||
/** | ||
* Set the locale of the current request. | ||
* | ||
* <p> | ||
* NOTE: The locale is stored in a ThreadLocal that must be cleaned up with {@link #clear()}. | ||
* </p> | ||
* | ||
* @param locale the locale of the current request. | ||
*/ | ||
public static void setLocale(Locale locale) { | ||
LOCALE.set(locale); | ||
} | ||
|
||
/** | ||
* Clear the locale of the current request. | ||
*/ | ||
public static void clear() { | ||
LOCALE.remove(); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
io.github.belgif.rest.problem.jaxrs.i18n.ThreadLocalLocaleResolver |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
package io.github.belgif.rest.problem.jaxrs.i18n; | ||
|
||
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.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() { | ||
ThreadLocalLocaleResolver.clear(); | ||
I18N.setEnabled(true); | ||
} | ||
|
||
@Test | ||
void languageRequested() { | ||
when(requestContext.getAcceptableLanguages()) | ||
.thenReturn(Collections.singletonList(Locale.forLanguageTag("nl-BE"))); | ||
filter.filter(requestContext); | ||
assertThat(I18N.getRequestLocale()).isEqualTo(Locale.forLanguageTag("nl-BE")); | ||
} | ||
|
||
@Test | ||
void noLanguageRequested() { | ||
when(requestContext.getAcceptableLanguages()).thenReturn(Collections.emptyList()); | ||
filter.filter(requestContext); | ||
assertThat(I18N.getRequestLocale()).isEqualTo(Locale.forLanguageTag("en")); | ||
} | ||
|
||
@Test | ||
void clearLocaleAfterResponse() { | ||
ThreadLocalLocaleResolver.setLocale(Locale.forLanguageTag("nl-BE")); | ||
filter.filter(requestContext, responseContext); | ||
assertThat(I18N.getRequestLocale()).isEqualTo(Locale.forLanguageTag("en")); | ||
} | ||
|
||
@Test | ||
void disabledViaInitParam() { | ||
when(servletContext.getInitParameter(I18N.I18N_FLAG)).thenReturn("false"); | ||
filter.initialize(); | ||
filter.filter(requestContext); | ||
jpraet marked this conversation as resolved.
Show resolved
Hide resolved
|
||
verifyNoInteractions(requestContext); | ||
} | ||
|
||
jpraet marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
package io.github.belgif.rest.problem.jaxrs.i18n; | ||
|
||
import static org.assertj.core.api.Assertions.*; | ||
|
||
import java.util.Locale; | ||
|
||
import org.junit.jupiter.api.AfterEach; | ||
import org.junit.jupiter.api.Test; | ||
|
||
class ThreadLocalLocaleResolverTest { | ||
|
||
private final ThreadLocalLocaleResolver resolver = new ThreadLocalLocaleResolver(); | ||
|
||
@AfterEach | ||
void cleanup() { | ||
ThreadLocalLocaleResolver.clear(); | ||
} | ||
|
||
@Test | ||
void defaultLocale() { | ||
assertThat(resolver.getLocale()).isEqualTo(Locale.forLanguageTag("en")); | ||
} | ||
|
||
@Test | ||
void setAndGetLocale() { | ||
Locale locale = Locale.forLanguageTag("nl-BE"); | ||
ThreadLocalLocaleResolver.setLocale(locale); | ||
assertThat(resolver.getLocale()).isEqualTo(locale); | ||
} | ||
|
||
@Test | ||
void clear() { | ||
Locale locale = Locale.forLanguageTag("nl-BE"); | ||
ThreadLocalLocaleResolver.setLocale(locale); | ||
ThreadLocalLocaleResolver.clear(); | ||
assertThat(resolver.getLocale()).isEqualTo(Locale.forLanguageTag("en")); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package io.github.belgif.rest.problem.spring.i18n; | ||
|
||
import org.springframework.stereotype.Component; | ||
|
||
import io.github.belgif.rest.problem.i18n.I18N; | ||
import io.github.belgif.rest.problem.spring.ProblemConfigurationProperties; | ||
|
||
/** | ||
* Enables or disables problem I18N support based on configuration property. | ||
*/ | ||
@Component | ||
public class I18NConfigurator { | ||
|
||
public I18NConfigurator(ProblemConfigurationProperties configuration) { | ||
I18N.setEnabled(configuration.isI18n()); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package io.github.belgif.rest.problem.spring.i18n; | ||
|
||
import java.util.Locale; | ||
|
||
import org.springframework.context.i18n.LocaleContextHolder; | ||
|
||
import io.github.belgif.rest.problem.i18n.LocaleResolver; | ||
|
||
/** | ||
* Spring LocaleResolver implementation that delegates to {@link LocaleContextHolder#getLocale()}. | ||
*/ | ||
public class LocaleContextHolderLocaleResolver implements LocaleResolver { | ||
|
||
@Override | ||
public Locale getLocale() { | ||
return LocaleContextHolder.getLocale(); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
io.github.belgif.rest.problem.spring.i18n.LocaleContextHolderLocaleResolver |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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).