Skip to content

Commit

Permalink
Support changing locale using query language (#6658)
Browse files Browse the repository at this point in the history
#### 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
JohnNiang committed Sep 14, 2024
1 parent 46793af commit c5f9c76
Show file tree
Hide file tree
Showing 2 changed files with 149 additions and 0 deletions.
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)));
}

}
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()
);
}
}

0 comments on commit c5f9c76

Please sign in to comment.