mirror of https://github.com/halo-dev/halo
feat: support configuring default locale in system setting (#7365)
#### What type of PR is this? /kind feature /area core /milestone 2.20.x #### What this PR does / why we need it: 系统设置新增首选语言设置 #### Which issue(s) this PR fixes: Fixes #7047 Fixes https://github.com/halo-dev/halo/issues/7172 Fixes https://github.com/halo-dev/halo/issues/4086 Fixes https://github.com/halo-dev/halo/issues/7336 #### Does this PR introduce a user-facing change? ```release-note 系统设置新增首选语言设置 ```pull/7372/head^2 v2.20.19
parent
23951de314
commit
0676551c77
|
@ -1,12 +1,16 @@
|
||||||
package run.halo.app.infra;
|
package run.halo.app.infra;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.boot.convert.ApplicationConversionService;
|
import org.springframework.boot.convert.ApplicationConversionService;
|
||||||
import run.halo.app.extension.ConfigMap;
|
import run.halo.app.extension.ConfigMap;
|
||||||
import run.halo.app.infra.utils.JsonUtils;
|
import run.halo.app.infra.utils.JsonUtils;
|
||||||
|
@ -66,6 +70,14 @@ public class SystemSetting {
|
||||||
String subtitle;
|
String subtitle;
|
||||||
String logo;
|
String logo;
|
||||||
String favicon;
|
String favicon;
|
||||||
|
String language;
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
public Optional<Locale> useSystemLocale() {
|
||||||
|
return Optional.ofNullable(language)
|
||||||
|
.filter(StringUtils::isNotBlank)
|
||||||
|
.map(Locale::forLanguageTag);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
|
|
@ -59,6 +59,11 @@ public class SystemConfigurableEnvironmentFetcher implements Reconciler<Reconcil
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Mono<SystemSetting.Basic> getBasic() {
|
||||||
|
return fetch(SystemSetting.Basic.GROUP, SystemSetting.Basic.class)
|
||||||
|
.switchIfEmpty(Mono.just(new SystemSetting.Basic()));
|
||||||
|
}
|
||||||
|
|
||||||
public Mono<SystemSetting.Comment> fetchComment() {
|
public Mono<SystemSetting.Comment> fetchComment() {
|
||||||
return fetch(SystemSetting.Comment.GROUP, SystemSetting.Comment.class)
|
return fetch(SystemSetting.Comment.GROUP, SystemSetting.Comment.class)
|
||||||
.switchIfEmpty(Mono.just(new SystemSetting.Comment()));
|
.switchIfEmpty(Mono.just(new SystemSetting.Comment()));
|
||||||
|
|
|
@ -119,6 +119,7 @@ public class GlobalInfoServiceImpl implements GlobalInfoService {
|
||||||
if (basic != null) {
|
if (basic != null) {
|
||||||
info.setFavicon(basic.getFavicon());
|
info.setFavicon(basic.getFavicon());
|
||||||
info.setSiteTitle(basic.getTitle());
|
info.setSiteTitle(basic.getTitle());
|
||||||
|
basic.useSystemLocale().ifPresent(info::setLocale);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,9 @@ import run.halo.app.infra.console.WebSocketRequestPredicate;
|
||||||
import run.halo.app.infra.properties.AttachmentProperties;
|
import run.halo.app.infra.properties.AttachmentProperties;
|
||||||
import run.halo.app.infra.properties.HaloProperties;
|
import run.halo.app.infra.properties.HaloProperties;
|
||||||
import run.halo.app.infra.webfilter.AdditionalWebFilterChainProxy;
|
import run.halo.app.infra.webfilter.AdditionalWebFilterChainProxy;
|
||||||
|
import run.halo.app.infra.webfilter.LocaleChangeWebFilter;
|
||||||
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
|
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
|
||||||
|
import run.halo.app.theme.UserLocaleRequestAttributeWriteFilter;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class WebFluxConfig implements WebFluxConfigurer {
|
public class WebFluxConfig implements WebFluxConfigurer {
|
||||||
|
@ -219,15 +221,22 @@ public class WebFluxConfig implements WebFluxConfigurer {
|
||||||
|
|
||||||
@ConditionalOnProperty(name = "halo.console.proxy.enabled", havingValue = "true")
|
@ConditionalOnProperty(name = "halo.console.proxy.enabled", havingValue = "true")
|
||||||
@Bean
|
@Bean
|
||||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
@Order(Ordered.HIGHEST_PRECEDENCE + 2)
|
||||||
ProxyFilter consoleProxyFilter() {
|
ProxyFilter consoleProxyFilter() {
|
||||||
return new ProxyFilter("/console/**", haloProp.getConsole().getProxy());
|
return new ProxyFilter("/console/**", haloProp.getConsole().getProxy());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Order of this filter is higher than
|
||||||
|
* {@link LocaleChangeWebFilter} to allow change locale in dev
|
||||||
|
* mode.
|
||||||
|
* {@link UserLocaleRequestAttributeWriteFilter} is before {@link LocaleChangeWebFilter} to
|
||||||
|
* obtain the locale
|
||||||
|
*/
|
||||||
@ConditionalOnProperty(name = "halo.uc.proxy.enabled", havingValue = "true")
|
@ConditionalOnProperty(name = "halo.uc.proxy.enabled", havingValue = "true")
|
||||||
@Bean
|
@Bean
|
||||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
@Order(Ordered.HIGHEST_PRECEDENCE + 2)
|
||||||
ProxyFilter ucProxyFilter() {
|
ProxyFilter ucProxyFilter() {
|
||||||
return new ProxyFilter("/uc/**", haloProp.getUc().getProxy());
|
return new ProxyFilter("/uc/**", haloProp.getUc().getProxy());
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
package run.halo.app.infra.webfilter;
|
package run.halo.app.infra.webfilter;
|
||||||
|
|
||||||
import static run.halo.app.theme.ThemeLocaleContextResolver.LANGUAGE_COOKIE_NAME;
|
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.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import org.springframework.core.Ordered;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseCookie;
|
import org.springframework.http.ResponseCookie;
|
||||||
|
@ -15,18 +16,26 @@ import org.springframework.security.web.server.util.matcher.ServerWebExchangeMat
|
||||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult;
|
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult;
|
||||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
|
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
import org.springframework.web.server.WebFilter;
|
import org.springframework.web.server.WebFilter;
|
||||||
import org.springframework.web.server.WebFilterChain;
|
import org.springframework.web.server.WebFilterChain;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
import run.halo.app.theme.ThemeLocaleContextResolver;
|
||||||
|
import run.halo.app.theme.UserLocaleRequestAttributeWriteFilter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link UserLocaleRequestAttributeWriteFilter} is before {@link LocaleChangeWebFilter} to
|
||||||
|
* obtain the locale.
|
||||||
|
*/
|
||||||
@Component
|
@Component
|
||||||
|
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
|
||||||
public class LocaleChangeWebFilter implements WebFilter {
|
public class LocaleChangeWebFilter implements WebFilter {
|
||||||
|
|
||||||
private final ServerWebExchangeMatcher matcher;
|
private final ServerWebExchangeMatcher matcher;
|
||||||
|
private final ThemeLocaleContextResolver themeLocaleContextResolver;
|
||||||
|
|
||||||
public LocaleChangeWebFilter() {
|
public LocaleChangeWebFilter(ThemeLocaleContextResolver themeLocaleContextResolver) {
|
||||||
|
this.themeLocaleContextResolver = themeLocaleContextResolver;
|
||||||
var pathMatcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/**");
|
var pathMatcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/**");
|
||||||
var textHtmlMatcher = new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML);
|
var textHtmlMatcher = new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML);
|
||||||
textHtmlMatcher.setIgnoredMediaTypes(Set.of(MediaType.ALL));
|
textHtmlMatcher.setIgnoredMediaTypes(Set.of(MediaType.ALL));
|
||||||
|
@ -35,16 +44,14 @@ public class LocaleChangeWebFilter implements WebFilter {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@NonNull
|
@NonNull
|
||||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
public Mono<Void> filter(ServerWebExchange exchange, @NonNull WebFilterChain chain) {
|
||||||
var request = exchange.getRequest();
|
var request = exchange.getRequest();
|
||||||
return matcher.matches(exchange)
|
return matcher.matches(exchange)
|
||||||
.filter(MatchResult::isMatch)
|
.filter(MatchResult::isMatch)
|
||||||
.doOnNext(result -> {
|
.doOnNext(result -> {
|
||||||
var language = request
|
var localeContext = themeLocaleContextResolver.resolveLocaleContext(exchange);
|
||||||
.getQueryParams()
|
var locale = localeContext.getLocale();
|
||||||
.getFirst(LANGUAGE_PARAMETER_NAME);
|
if (locale != null) {
|
||||||
if (StringUtils.hasText(language)) {
|
|
||||||
var locale = Locale.forLanguageTag(language);
|
|
||||||
setLanguageCookie(exchange, locale);
|
setLanguageCookie(exchange, locale);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -20,6 +20,8 @@ import run.halo.app.core.extension.notification.ReasonType;
|
||||||
import run.halo.app.core.extension.notification.Subscription;
|
import run.halo.app.core.extension.notification.Subscription;
|
||||||
import run.halo.app.extension.Metadata;
|
import run.halo.app.extension.Metadata;
|
||||||
import run.halo.app.extension.ReactiveExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
|
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||||
|
import run.halo.app.infra.SystemSetting;
|
||||||
import run.halo.app.notification.endpoint.SubscriptionRouter;
|
import run.halo.app.notification.endpoint.SubscriptionRouter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -41,6 +43,7 @@ public class DefaultNotificationCenter implements NotificationCenter {
|
||||||
private final SubscriptionRouter subscriptionRouter;
|
private final SubscriptionRouter subscriptionRouter;
|
||||||
private final RecipientResolver recipientResolver;
|
private final RecipientResolver recipientResolver;
|
||||||
private final SubscriptionService subscriptionService;
|
private final SubscriptionService subscriptionService;
|
||||||
|
private final SystemConfigurableEnvironmentFetcher environmentFetcher;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<Void> notify(Reason reason) {
|
public Mono<Void> notify(Reason reason) {
|
||||||
|
@ -287,6 +290,8 @@ public class DefaultNotificationCenter implements NotificationCenter {
|
||||||
|
|
||||||
Mono<Locale> getLocaleFromSubscriber(Subscriber subscriber) {
|
Mono<Locale> getLocaleFromSubscriber(Subscriber subscriber) {
|
||||||
// TODO get locale from subscriber
|
// TODO get locale from subscriber
|
||||||
return Mono.just(Locale.getDefault());
|
return environmentFetcher.getBasic()
|
||||||
|
.map(SystemSetting.Basic::useSystemLocale)
|
||||||
|
.map(localeOpt -> localeOpt.orElse(Locale.getDefault()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -212,6 +212,7 @@ public class SystemSetupEndpoint {
|
||||||
String basic = data.getOrDefault(SystemSetting.Basic.GROUP, "{}");
|
String basic = data.getOrDefault(SystemSetting.Basic.GROUP, "{}");
|
||||||
var basicSetting = JsonUtils.jsonToObject(basic, SystemSetting.Basic.class);
|
var basicSetting = JsonUtils.jsonToObject(basic, SystemSetting.Basic.class);
|
||||||
basicSetting.setTitle(body.getSiteTitle());
|
basicSetting.setTitle(body.getSiteTitle());
|
||||||
|
basicSetting.setLanguage(body.getLanguage());
|
||||||
data.put(SystemSetting.Basic.GROUP, JsonUtils.objectToJson(basicSetting));
|
data.put(SystemSetting.Basic.GROUP, JsonUtils.objectToJson(basicSetting));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -272,6 +273,11 @@ public class SystemSetupEndpoint {
|
||||||
public String getSiteTitle() {
|
public String getSiteTitle() {
|
||||||
return formData.getFirst("siteTitle");
|
return formData.getFirst("siteTitle");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Pattern(regexp = "^(zh-CN|zh-TW|en|es)$")
|
||||||
|
public String getLanguage() {
|
||||||
|
return formData.getFirst("language");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Flux<Unstructured> loadPresetExtensions(String username) {
|
Flux<Unstructured> loadPresetExtensions(String username) {
|
||||||
|
|
|
@ -29,12 +29,14 @@ public class ThemeLocaleContextResolver extends AcceptHeaderLocaleContextResolve
|
||||||
|
|
||||||
public static final String TIME_ZONE_COOKIE_NAME = "time_zone";
|
public static final String TIME_ZONE_COOKIE_NAME = "time_zone";
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@NonNull
|
@NonNull
|
||||||
public LocaleContext resolveLocaleContext(@NonNull ServerWebExchange exchange) {
|
public LocaleContext resolveLocaleContext(@NonNull ServerWebExchange exchange) {
|
||||||
var request = exchange.getRequest();
|
var request = exchange.getRequest();
|
||||||
var locale = getLocaleFromQueryParameter(request)
|
var locale = getLocaleFromQueryParameter(request)
|
||||||
.or(() -> getLocaleFromCookie(request))
|
.or(() -> getLocaleFromCookie(request))
|
||||||
|
.or(() -> UserLocaleRequestAttributeWriteFilter.getUserLocale(request))
|
||||||
.orElseGet(() -> super.resolveLocaleContext(exchange).getLocale());
|
.orElseGet(() -> super.resolveLocaleContext(exchange).getLocale());
|
||||||
|
|
||||||
var timeZone = getTimeZoneFromCookie(request)
|
var timeZone = getTimeZoneFromCookie(request)
|
||||||
|
@ -62,5 +64,4 @@ public class ThemeLocaleContextResolver extends AcceptHeaderLocaleContextResolve
|
||||||
.filter(StringUtils::isNotBlank)
|
.filter(StringUtils::isNotBlank)
|
||||||
.map(TimeZone::getTimeZone);
|
.map(TimeZone::getTimeZone);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
package run.halo.app.theme;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Optional;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.core.Ordered;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
import org.springframework.web.server.WebFilter;
|
||||||
|
import org.springframework.web.server.WebFilterChain;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||||
|
import run.halo.app.infra.SystemSetting;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserLocaleRequestAttributeWriteFilter implements WebFilter {
|
||||||
|
public static final String USER_LOCALE_ATTRIBUTE =
|
||||||
|
UserLocaleRequestAttributeWriteFilter.class.getName() + ".USER_LOCALE_ATTRIBUTE";
|
||||||
|
|
||||||
|
private final SystemConfigurableEnvironmentFetcher environmentFetcher;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NonNull
|
||||||
|
public Mono<Void> filter(@NonNull ServerWebExchange exchange, @NonNull WebFilterChain chain) {
|
||||||
|
return environmentFetcher.getBasic()
|
||||||
|
.map(SystemSetting.Basic::useSystemLocale)
|
||||||
|
.doOnNext(localeOpt -> localeOpt
|
||||||
|
.ifPresent(locale -> exchange.getAttributes().put(USER_LOCALE_ATTRIBUTE, locale))
|
||||||
|
)
|
||||||
|
.then(chain.filter(exchange));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<Locale> getUserLocale(ServerHttpRequest request) {
|
||||||
|
return Optional.ofNullable((Locale) request.getAttributes()
|
||||||
|
.get(USER_LOCALE_ATTRIBUTE));
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package run.halo.app.theme.finders.vo;
|
package run.halo.app.theme.finders.vo;
|
||||||
|
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Value;
|
import lombok.Value;
|
||||||
|
@ -34,6 +35,8 @@ public class SiteSettingVo {
|
||||||
|
|
||||||
String favicon;
|
String favicon;
|
||||||
|
|
||||||
|
String language;
|
||||||
|
|
||||||
Boolean allowRegistration;
|
Boolean allowRegistration;
|
||||||
|
|
||||||
PostSetting post;
|
PostSetting post;
|
||||||
|
@ -74,6 +77,7 @@ public class SiteSettingVo {
|
||||||
.logo(basicSetting.getLogo())
|
.logo(basicSetting.getLogo())
|
||||||
.favicon(basicSetting.getFavicon())
|
.favicon(basicSetting.getFavicon())
|
||||||
.allowRegistration(userSetting.isAllowRegistration())
|
.allowRegistration(userSetting.isAllowRegistration())
|
||||||
|
.language(basicSetting.useSystemLocale().orElse(Locale.getDefault()).toLanguageTag())
|
||||||
.post(PostSetting.builder()
|
.post(PostSetting.builder()
|
||||||
.postPageSize(postSetting.getPostPageSize())
|
.postPageSize(postSetting.getPostPageSize())
|
||||||
.archivePageSize(postSetting.getArchivePageSize())
|
.archivePageSize(postSetting.getArchivePageSize())
|
||||||
|
|
|
@ -24,6 +24,19 @@ spec:
|
||||||
name: favicon
|
name: favicon
|
||||||
accepts:
|
accepts:
|
||||||
- 'image/*'
|
- 'image/*'
|
||||||
|
- $formkit: select
|
||||||
|
label: "首选语言"
|
||||||
|
name: language
|
||||||
|
value: 'zh-CN'
|
||||||
|
options:
|
||||||
|
- label: 'English'
|
||||||
|
value: 'en'
|
||||||
|
- label: 'Español'
|
||||||
|
value: 'es'
|
||||||
|
- label: '简体中文'
|
||||||
|
value: 'zh-CN'
|
||||||
|
- label: '繁体中文'
|
||||||
|
value: 'zh-TW'
|
||||||
- group: post
|
- group: post
|
||||||
label: 文章设置
|
label: 文章设置
|
||||||
formSchema:
|
formSchema:
|
||||||
|
|
|
@ -107,6 +107,19 @@
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.halo-form .form-item select {
|
||||||
|
appearance: none;
|
||||||
|
font-size: var(--text-base);
|
||||||
|
box-shadow: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='m12 13.171l4.95-4.95l1.414 1.415L12 16L5.636 9.636L7.05 8.222z'/%3E%3C/svg%3E")
|
||||||
|
right 0em center no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
.halo-form .form-item input:focus {
|
.halo-form .form-item input:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html
|
<html
|
||||||
xmlns:th="https://www.thymeleaf.org"
|
xmlns:th="https://www.thymeleaf.org"
|
||||||
th:replace="~{gateway_fragments/layout :: layout(title = |#{title} - Halo|, head = ~{::head}, body = ~{::body})}"
|
th:replace="~{gateway_fragments/layout :: layout(title = |#{title} - Halo|, head = ~{::head}, body = ~{::body})}"
|
||||||
|
@ -22,6 +22,18 @@
|
||||||
<span th:text="#{form.messages.h2.content}"> </span>
|
<span th:text="#{form.messages.h2.content}"> </span>
|
||||||
</div>
|
</div>
|
||||||
<form th:object="${form}" th:action="@{/system/setup}" class="halo-form" method="post">
|
<form th:object="${form}" th:action="@{/system/setup}" class="halo-form" method="post">
|
||||||
|
<div class="form-item">
|
||||||
|
<label for="language" th:text="#{form.language.label}"></label>
|
||||||
|
<div class="form-input">
|
||||||
|
<select name="language" id="language">
|
||||||
|
<option value="en" th:selected="${#locale.toLanguageTag} == 'en'">English</option>
|
||||||
|
<option value="es" th:selected="${#locale.toLanguageTag} == 'es'">Español</option>
|
||||||
|
<option value="zh-CN" th:selected="${#locale.toLanguageTag} == 'zh-CN'">简体中文</option>
|
||||||
|
<option value="zh-TW" th:selected="${#locale.toLanguageTag} == 'zh-TW'">繁体中文</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
<label for="siteTitle" th:text="#{form.siteTitle.label}"></label>
|
<label for="siteTitle" th:text="#{form.siteTitle.label}"></label>
|
||||||
<div class="form-input">
|
<div class="form-input">
|
||||||
|
@ -112,14 +124,20 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div th:replace="~{gateway_fragments/common::languageSwitcher}"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script th:inline="javascript">
|
<script th:inline="javascript">
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
setupPasswordConfirmation("password", "confirmPassword");
|
setupPasswordConfirmation("password", "confirmPassword");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById("language").addEventListener("change", function () {
|
||||||
|
const selectedLanguage = this.value;
|
||||||
|
const currentURL = new URL(window.location.href);
|
||||||
|
currentURL.searchParams.set("language", selectedLanguage);
|
||||||
|
history.replaceState(null, "", currentURL.toString());
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</th:block>
|
</th:block>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
title=系统初始化
|
title=系统初始化
|
||||||
|
form.language.label=语言
|
||||||
form.siteTitle.label=站点标题
|
form.siteTitle.label=站点标题
|
||||||
form.username.label=用户名
|
form.username.label=用户名
|
||||||
form.email.label=电子邮箱
|
form.email.label=电子邮箱
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
title=Setup
|
title=Setup
|
||||||
|
form.language.label=Language
|
||||||
form.siteTitle.label=Site title
|
form.siteTitle.label=Site title
|
||||||
form.username.label=Username
|
form.username.label=Username
|
||||||
form.email.label=Email
|
form.email.label=Email
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
title=Configuración
|
title=Configuración
|
||||||
|
form.language.label=Idioma
|
||||||
form.siteTitle.label=Título del Sitio
|
form.siteTitle.label=Título del Sitio
|
||||||
form.username.label=Nombre de Usuario
|
form.username.label=Nombre de Usuario
|
||||||
form.email.label=Correo Electrónico
|
form.email.label=Correo Electrónico
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
title=系統初始化
|
title=系統初始化
|
||||||
|
form.language.label=語言
|
||||||
form.siteTitle.label=站點標題
|
form.siteTitle.label=站點標題
|
||||||
form.username.label=使用者名稱
|
form.username.label=使用者名稱
|
||||||
form.email.label=電子郵件
|
form.email.label=電子郵件
|
||||||
|
|
|
@ -30,6 +30,8 @@ import run.halo.app.core.extension.notification.ReasonType;
|
||||||
import run.halo.app.core.extension.notification.Subscription;
|
import run.halo.app.core.extension.notification.Subscription;
|
||||||
import run.halo.app.extension.Metadata;
|
import run.halo.app.extension.Metadata;
|
||||||
import run.halo.app.extension.ReactiveExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
|
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||||
|
import run.halo.app.infra.SystemSetting;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link DefaultNotificationCenter}.
|
* Tests for {@link DefaultNotificationCenter}.
|
||||||
|
@ -61,6 +63,9 @@ class DefaultNotificationCenterTest {
|
||||||
@Mock
|
@Mock
|
||||||
private SubscriptionService subscriptionService;
|
private SubscriptionService subscriptionService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private SystemConfigurableEnvironmentFetcher environmentFetcher;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private DefaultNotificationCenter notificationCenter;
|
private DefaultNotificationCenter notificationCenter;
|
||||||
|
|
||||||
|
@ -317,6 +322,7 @@ class DefaultNotificationCenterTest {
|
||||||
void getLocaleFromSubscriberTest() {
|
void getLocaleFromSubscriberTest() {
|
||||||
var subscription = mock(Subscriber.class);
|
var subscription = mock(Subscriber.class);
|
||||||
|
|
||||||
|
when(environmentFetcher.getBasic()).thenReturn(Mono.just(new SystemSetting.Basic()));
|
||||||
notificationCenter.getLocaleFromSubscriber(subscription)
|
notificationCenter.getLocaleFromSubscriber(subscription)
|
||||||
.as(StepVerifier::create)
|
.as(StepVerifier::create)
|
||||||
.expectNext(Locale.getDefault())
|
.expectNext(Locale.getDefault())
|
||||||
|
|
|
@ -15,6 +15,7 @@ import org.springframework.mock.web.server.MockServerWebExchange;
|
||||||
import org.springframework.web.server.WebFilterChain;
|
import org.springframework.web.server.WebFilterChain;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.infra.webfilter.LocaleChangeWebFilter;
|
import run.halo.app.infra.webfilter.LocaleChangeWebFilter;
|
||||||
|
import run.halo.app.theme.ThemeLocaleContextResolver;
|
||||||
|
|
||||||
class LocaleChangeWebFilterTest {
|
class LocaleChangeWebFilterTest {
|
||||||
|
|
||||||
|
@ -22,7 +23,8 @@ class LocaleChangeWebFilterTest {
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
filter = new LocaleChangeWebFilter();
|
var themeLocaleContextResolver = new ThemeLocaleContextResolver();
|
||||||
|
filter = new LocaleChangeWebFilter(themeLocaleContextResolver);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -16,7 +16,7 @@ import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
const SYSTEM_CONFIGMAP_NAME = "system";
|
const SYSTEM_CONFIGMAP_NAME = "system";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t, locale } = useI18n();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const group = inject<Ref<string>>("activeTab", ref("basic"));
|
const group = inject<Ref<string>>("activeTab", ref("basic"));
|
||||||
|
@ -60,6 +60,10 @@ const handleSaveConfigMap = async () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["system-configMap"] });
|
queryClient.invalidateQueries({ queryKey: ["system-configMap"] });
|
||||||
await useGlobalInfoStore().fetchGlobalInfo();
|
await useGlobalInfoStore().fetchGlobalInfo();
|
||||||
|
|
||||||
|
const language = configMapFormData.value.basic.language;
|
||||||
|
locale.value = language;
|
||||||
|
document.cookie = `language=${language}; path=/; SameSite=Lax; Secure`;
|
||||||
|
|
||||||
saving.value = false;
|
saving.value = false;
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
Loading…
Reference in New Issue