mirror of https://github.com/halo-dev/halo
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 切换地域语言 ```pull/6671/head
parent
46793af0bd
commit
c5f9c766bb
|
@ -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)));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue