-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support changing locale using query language (#6658)
#### What type of PR is this? /kind improvement /area core /milestone 2.20.x #### What this PR does / why we need it: This PR adds support changing locale using query `language`. After passing the query, we will automatically respond a cookie `language` back to browser. Please see the result below: ```bash http http://localhost:8090/\?language\=zh-CN Accept:text/html -p h HTTP/1.1 200 OK Cache-Control: no-cache, no-store, max-age=0, must-revalidate Content-Language: zh-CN Content-Type: text/html Expires: 0 Pragma: no-cache Referrer-Policy: strict-origin-when-cross-origin Vary: Origin Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers X-Content-Type-Options: nosniff X-Frame-Options: SAMEORIGIN X-XSS-Protection: 0 content-encoding: gzip content-length: 4765 set-cookie: language=zh-CN; Path=/; Secure set-cookie: XSRF-TOKEN=f0f2c972-0024-4575-aef2-0609356b4757; Path=/ ``` #### Does this PR introduce a user-facing change? ```release-note 支持利用参数 language 切换地域语言 ```
- Loading branch information
Showing
2 changed files
with
149 additions
and
0 deletions.
There are no files selected for viewing
59 changes: 59 additions & 0 deletions
59
application/src/main/java/run/halo/app/webfilter/LocaleChangeWebFilter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
package run.halo.app.webfilter; | ||
|
||
import static run.halo.app.theme.ThemeLocaleContextResolver.LANGUAGE_COOKIE_NAME; | ||
import static run.halo.app.theme.ThemeLocaleContextResolver.LANGUAGE_PARAMETER_NAME; | ||
|
||
import java.util.Locale; | ||
import java.util.Set; | ||
import org.springframework.http.HttpMethod; | ||
import org.springframework.http.MediaType; | ||
import org.springframework.http.ResponseCookie; | ||
import org.springframework.lang.NonNull; | ||
import org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher; | ||
import org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher; | ||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; | ||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult; | ||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.util.StringUtils; | ||
import org.springframework.web.server.ServerWebExchange; | ||
import org.springframework.web.server.WebFilter; | ||
import org.springframework.web.server.WebFilterChain; | ||
import reactor.core.publisher.Mono; | ||
|
||
@Component | ||
public class LocaleChangeWebFilter implements WebFilter { | ||
|
||
private final ServerWebExchangeMatcher matcher; | ||
|
||
public LocaleChangeWebFilter() { | ||
var pathMatcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/**"); | ||
var textHtmlMatcher = new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML); | ||
textHtmlMatcher.setIgnoredMediaTypes(Set.of(MediaType.ALL)); | ||
matcher = new AndServerWebExchangeMatcher(pathMatcher, textHtmlMatcher); | ||
} | ||
|
||
@Override | ||
@NonNull | ||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { | ||
var request = exchange.getRequest(); | ||
return matcher.matches(exchange) | ||
.filter(MatchResult::isMatch) | ||
.doOnNext(result -> { | ||
var language = request | ||
.getQueryParams() | ||
.getFirst(LANGUAGE_PARAMETER_NAME); | ||
if (StringUtils.hasText(language)) { | ||
var locale = Locale.forLanguageTag(language); | ||
exchange.getResponse() | ||
.addCookie(ResponseCookie.from(LANGUAGE_COOKIE_NAME, locale.toLanguageTag()) | ||
.path("/") | ||
.secure(true) | ||
.build() | ||
); | ||
} | ||
}) | ||
.then(Mono.defer(() -> chain.filter(exchange))); | ||
} | ||
|
||
} |
90 changes: 90 additions & 0 deletions
90
application/src/test/java/run/halo/app/webfilter/LocaleChangeWebFilterTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
package run.halo.app.webfilter; | ||
|
||
import static org.junit.jupiter.api.Assertions.assertEquals; | ||
import static org.junit.jupiter.api.Assertions.assertNotNull; | ||
import static org.junit.jupiter.api.Assertions.assertNull; | ||
|
||
import java.util.stream.Stream; | ||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.params.ParameterizedTest; | ||
import org.junit.jupiter.params.provider.MethodSource; | ||
import org.springframework.http.MediaType; | ||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest; | ||
import org.springframework.mock.web.server.MockServerWebExchange; | ||
import org.springframework.web.server.WebFilterChain; | ||
import reactor.core.publisher.Mono; | ||
|
||
class LocaleChangeWebFilterTest { | ||
|
||
LocaleChangeWebFilter filter; | ||
|
||
@BeforeEach | ||
void setUp() { | ||
filter = new LocaleChangeWebFilter(); | ||
} | ||
|
||
@Test | ||
void shouldRespondLanguageCookie() { | ||
WebFilterChain webFilterChain = filterExchange -> { | ||
var languageCookie = filterExchange.getResponse().getCookies().getFirst("language"); | ||
assertNotNull(languageCookie); | ||
assertEquals("zh-CN", languageCookie.getValue()); | ||
return Mono.empty(); | ||
}; | ||
var exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/home") | ||
.accept(MediaType.TEXT_HTML) | ||
.queryParam("language", "zh-CN") | ||
.build() | ||
); | ||
this.filter.filter(exchange, webFilterChain).block(); | ||
} | ||
|
||
@Test | ||
void shouldRespondLanguageCookieWithUndefinedLanguageTag() { | ||
WebFilterChain webFilterChain = filterExchange -> { | ||
var languageCookie = filterExchange.getResponse().getCookies().getFirst("language"); | ||
assertNotNull(languageCookie); | ||
assertEquals("und", languageCookie.getValue()); | ||
return Mono.empty(); | ||
}; | ||
var exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/home") | ||
.accept(MediaType.TEXT_HTML) | ||
.queryParam("language", "invalid_language_tag") | ||
.build() | ||
); | ||
this.filter.filter(exchange, webFilterChain).block(); | ||
} | ||
|
||
@ParameterizedTest | ||
@MethodSource("provideInvalidRequest") | ||
void shouldNotRespondLanguageCookieIfRequestNotMatch(MockServerHttpRequest mockRequest) { | ||
WebFilterChain webFilterChain = filterExchange -> { | ||
var languageCookie = filterExchange.getResponse().getCookies().getFirst("language"); | ||
assertNull(languageCookie); | ||
return Mono.empty(); | ||
}; | ||
var exchange = MockServerWebExchange.from(mockRequest); | ||
this.filter.filter(exchange, webFilterChain).block(); | ||
} | ||
|
||
static Stream<MockServerHttpRequest> provideInvalidRequest() { | ||
return Stream.of( | ||
MockServerHttpRequest.get("/home") | ||
.accept(MediaType.ALL) | ||
.queryParam("language", "zh-CN") | ||
.build(), | ||
MockServerHttpRequest.get("/home") | ||
.accept(MediaType.APPLICATION_JSON) | ||
.queryParam("language", "zh-CN") | ||
.build(), | ||
MockServerHttpRequest.post("/home") | ||
.accept(MediaType.TEXT_HTML) | ||
.queryParam("language", "zh-CN") | ||
.build(), | ||
MockServerHttpRequest.get("/home") | ||
.accept(MediaType.TEXT_HTML) | ||
.build() | ||
); | ||
} | ||
} |