浏览代码

优化i18n

zhou-hao 3 年之前
父节点
当前提交
8b211de5e4
共有 25 个文件被更改,包括 306 次插入88 次删除
  1. 1 1
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/TwoFactor.java
  2. 20 0
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/configuration/AuthorizationI18nConfiguration.java
  3. 2 1
      hsweb-authorization/hsweb-authorization-api/src/main/resources/META-INF/spring.factories
  4. 16 0
      hsweb-authorization/hsweb-authorization-api/src/main/resources/i18n/authentication/messages_en_US.properties
  5. 6 4
      hsweb-authorization/hsweb-authorization-api/src/main/resources/i18n/messages_zh_CN.properties
  6. 1 2
      hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/twofactor/TwoFactorHandlerInterceptorAdapter.java
  7. 2 3
      hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/AuthorizationController.java
  8. 1 1
      hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/OAuth2Exception.java
  9. 20 0
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/Commons18nConfiguration.java
  10. 58 33
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonErrorControllerAdvice.java
  11. 2 7
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonWebFluxConfiguration.java
  12. 2 1
      hsweb-commons/hsweb-commons-crud/src/main/resources/META-INF/spring.factories
  13. 5 0
      hsweb-commons/hsweb-commons-crud/src/main/resources/i18n/commons/messages_en_US.properties
  14. 5 0
      hsweb-commons/hsweb-commons-crud/src/main/resources/i18n/commons/messages_zh_CN.properties
  15. 27 22
      hsweb-core/src/main/java/org/hswebframework/web/dict/EnumDict.java
  16. 0 4
      hsweb-core/src/main/java/org/hswebframework/web/exception/BusinessException.java
  17. 2 2
      hsweb-core/src/main/java/org/hswebframework/web/exception/ValidationException.java
  18. 3 3
      hsweb-core/src/main/java/org/hswebframework/web/i18n/LocaleUtils.java
  19. 16 1
      hsweb-core/src/main/java/org/hswebframework/web/i18n/WebFluxLocaleFilter.java
  20. 3 0
      hsweb-core/src/main/resources/i18n/core/messages_en_US.properties
  21. 4 0
      hsweb-core/src/main/resources/i18n/core/messages_zh_CN.properties
  22. 0 2
      hsweb-core/src/main/resources/i18n/messages_zh_CN.properties
  23. 70 0
      hsweb-starter/src/main/java/org/hswebframework/web/starter/i18n/CompositeMessageSource.java
  24. 38 0
      hsweb-starter/src/main/java/org/hswebframework/web/starter/i18n/I18nConfiguration.java
  25. 2 1
      hsweb-starter/src/main/resources/META-INF/spring.factories

+ 1 - 1
hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/TwoFactor.java

@@ -55,5 +55,5 @@ public @interface TwoFactor {
      * @return 错误提示
      * @since 3.0.6
      */
-    String message() default "assert.verify_code_error";
+    String message() default "validation.verify_code_error";
 }

+ 20 - 0
hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/configuration/AuthorizationI18nConfiguration.java

@@ -0,0 +1,20 @@
+package org.hswebframework.web.authorization.configuration;
+
+import org.springframework.context.MessageSource;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.support.ResourceBundleMessageSource;
+
+@Configuration
+public class AuthorizationI18nConfiguration {
+
+    @Bean
+    public MessageSource authorizationMessageSource(){
+        ResourceBundleMessageSource messageSource=new ResourceBundleMessageSource();
+        messageSource.setDefaultEncoding("UTF-8");
+        messageSource.setBundleClassLoader(AuthorizationI18nConfiguration.class.getClassLoader());
+        messageSource.setBasenames("i18n/authentication/messages");
+        return messageSource;
+    }
+
+}

+ 2 - 1
hsweb-authorization/hsweb-authorization-api/src/main/resources/META-INF/spring.factories

@@ -1,3 +1,4 @@
 # Auto Configure
 org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
-org.hswebframework.web.authorization.simple.DefaultAuthorizationAutoConfiguration
+org.hswebframework.web.authorization.simple.DefaultAuthorizationAutoConfiguration,\
+org.hswebframework.web.authorization.configuration.AuthorizationI18nConfiguration

+ 16 - 0
hsweb-authorization/hsweb-authorization-api/src/main/resources/i18n/authentication/messages_en_US.properties

@@ -0,0 +1,16 @@
+error.access_denied=Access Denied
+error.permission_denied=Permission Denied [{0}]:{1}
+error.logged_in_elsewhere=User logged in elsewhere
+error.illegal_password=Bad username or password
+error.user_disabled=User is disabled
+#
+message.token_state_normal=Normal
+message.token_state_deny=Login has denied
+message.token_state_expired=Login has expired
+message.token_state_offline=User logged in elsewhere
+message.token_state_lock=User Locked
+#
+validation.need_two_factor_verify=Two factor verification required
+validation.username_must_not_be_empty=Username must not be empty
+validation.password_must_not_be_empty=Password must not be empty
+validation.verify_code_error=Verification code error

+ 6 - 4
hsweb-authorization/hsweb-authorization-api/src/main/resources/i18n/messages_zh_CN.properties

@@ -1,6 +1,8 @@
 error.access_denied=权限不足,拒绝访问!
 error.permission_denied=当前用户无权限[{0}]:{1}
 error.logged_in_elsewhere=该用户已在其他地方登陆
+error.illegal_password=用户名或密码错误
+error.user_disabled=用户已被禁用
 #
 message.token_state_normal=正常
 message.token_state_deny=已被禁止访问
@@ -8,7 +10,7 @@ message.token_state_expired=用户未登录
 message.token_state_offline=用户已在其他地方登录
 message.token_state_lock=登录状态已被锁定
 #
-assert.need_two_factor_verify=需要双因子验证
-assert.username_must_not_be_empty=用户名不能为空
-assert.password_must_not_be_empty=密码不能为空
-assert.verify_code_error=验证码错误
+validation.need_two_factor_verify=需要双因子验证
+validation.username_must_not_be_empty=用户名不能为空
+validation.password_must_not_be_empty=密码不能为空
+validation.verify_code_error=验证码错误

+ 1 - 2
hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/twofactor/TwoFactorHandlerInterceptorAdapter.java

@@ -7,7 +7,6 @@ import org.hswebframework.web.authorization.annotation.TwoFactor;
 import org.hswebframework.web.authorization.exception.NeedTwoFactorException;
 import org.hswebframework.web.authorization.twofactor.TwoFactorValidator;
 import org.hswebframework.web.authorization.twofactor.TwoFactorValidatorManager;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.util.StringUtils;
 import org.springframework.web.method.HandlerMethod;
 import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
@@ -45,7 +44,7 @@ public class TwoFactorHandlerInterceptorAdapter extends HandlerInterceptorAdapte
                 code = request.getHeader(factor.parameter());
             }
             if (StringUtils.isEmpty(code)) {
-                throw new NeedTwoFactorException("assert.need_two_factor_verify", factor.provider());
+                throw new NeedTwoFactorException("validation.need_two_factor_verify", factor.provider());
             } else if (!validator.verify(code, factor.timeout())) {
                 throw new NeedTwoFactorException(factor.message(), factor.provider());
             }

+ 2 - 3
hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/AuthorizationController.java

@@ -34,7 +34,6 @@ import org.hswebframework.web.authorization.simple.PlainTextUsernamePasswordAuth
 import org.hswebframework.web.logging.AccessLogger;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.ApplicationEventPublisher;
-import org.springframework.context.event.EventListener;
 import org.springframework.http.MediaType;
 import org.springframework.util.Assert;
 import org.springframework.web.bind.annotation.*;
@@ -85,8 +84,8 @@ public class AuthorizationController {
             String username_ = (String) parameters.get("username");
             String password_ = (String) parameters.get("password");
 
-            Assert.hasLength(username_, "assert.username_must_not_be_empty");
-            Assert.hasLength(password_, "assert.password_must_not_be_empty");
+            Assert.hasLength(username_, "validation.username_must_not_be_empty");
+            Assert.hasLength(password_, "validation.password_must_not_be_empty");
 
             Function<String, Object> parameterGetter = parameters::get;
             return Mono.defer(() -> {

+ 1 - 1
hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/OAuth2Exception.java

@@ -8,7 +8,7 @@ public class OAuth2Exception extends BusinessException {
     private final ErrorType type;
 
     public OAuth2Exception(ErrorType type) {
-        super(type.message(), type.name(), type.code());
+        super(type.message(), type.name(), type.code(), (Object[]) null);
         this.type = type;
     }
 

+ 20 - 0
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/Commons18nConfiguration.java

@@ -0,0 +1,20 @@
+package org.hswebframework.web.crud.configuration;
+
+import org.springframework.context.MessageSource;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.support.ResourceBundleMessageSource;
+
+@Configuration
+public class Commons18nConfiguration {
+
+    @Bean
+    public MessageSource commonsMessageSource(){
+        ResourceBundleMessageSource messageSource=new ResourceBundleMessageSource();
+        messageSource.setDefaultEncoding("UTF-8");
+        messageSource.setBundleClassLoader(Commons18nConfiguration.class.getClassLoader());
+        messageSource.setBasenames("i18n/commons/messages");
+        return messageSource;
+    }
+
+}

+ 58 - 33
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonErrorControllerAdvice.java

@@ -22,10 +22,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler;
 import org.springframework.web.bind.annotation.ResponseStatus;
 import org.springframework.web.bind.annotation.RestControllerAdvice;
 import org.springframework.web.bind.support.WebExchangeBindException;
-import org.springframework.web.server.MediaTypeNotSupportedStatusException;
-import org.springframework.web.server.MethodNotAllowedException;
-import org.springframework.web.server.NotAcceptableStatusException;
-import org.springframework.web.server.ServerWebInputException;
+import org.springframework.web.server.*;
 import reactor.core.publisher.Mono;
 
 import javax.validation.ConstraintViolationException;
@@ -39,7 +36,6 @@ import java.util.stream.Collectors;
 @Order
 public class CommonErrorControllerAdvice {
 
-
     private final MessageSource messageSource;
 
     public CommonErrorControllerAdvice(MessageSource messageSource) {
@@ -58,25 +54,31 @@ public class CommonErrorControllerAdvice {
     @ExceptionHandler
     @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
     public Mono<ResponseMessage<?>> handleException(UnsupportedOperationException e) {
-        return Mono.just(ResponseMessage.error("unsupported", e.getMessage()));
+        return LocaleUtils
+                .resolveThrowable(messageSource, e, (err, msg) -> (ResponseMessage.<TokenState>error(401, "unsupported", msg)));
     }
 
     @ExceptionHandler
     @ResponseStatus(HttpStatus.UNAUTHORIZED)
     public Mono<ResponseMessage<TokenState>> handleException(UnAuthorizedException e) {
-        return Mono.just(ResponseMessage.<TokenState>error(401, "unauthorized", e.getMessage()).result(e.getState()));
+        return LocaleUtils
+                .resolveThrowable(messageSource, e, (err, msg) -> (ResponseMessage.<TokenState>error(401, "unauthorized", msg).result(e.getState())));
     }
 
     @ExceptionHandler
     @ResponseStatus(HttpStatus.FORBIDDEN)
-    public Mono<ResponseMessage<?>> handleException(AccessDenyException e) {
-        return Mono.just(ResponseMessage.error(403, e.getCode(), e.getMessage()));
+    public Mono<ResponseMessage<Object>> handleException(AccessDenyException e) {
+        return LocaleUtils
+                .resolveThrowable(messageSource, e, (err, msg) -> ResponseMessage.error(403, e.getCode(), e.getMessage()))
+                ;
     }
 
     @ExceptionHandler
     @ResponseStatus(HttpStatus.NOT_FOUND)
-    public Mono<ResponseMessage<?>> handleException(NotFoundException e) {
-        return Mono.just(ResponseMessage.error(404, "not_found", e.getMessage()));
+    public Mono<ResponseMessage<Object>> handleException(NotFoundException e) {
+        return LocaleUtils
+                .resolveThrowable(messageSource, e, (err, msg) -> ResponseMessage.error(404, "not_found", msg))
+                ;
     }
 
     @ExceptionHandler
@@ -138,6 +140,7 @@ public class CommonErrorControllerAdvice {
     @ExceptionHandler
     @ResponseStatus(HttpStatus.GATEWAY_TIMEOUT)
     public Mono<ResponseMessage<Object>> handleException(TimeoutException e) {
+
         return Mono.just(ResponseMessage.error(504, "timeout", e.getMessage()))
                    .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getMessage(), e)));
 
@@ -163,51 +166,70 @@ public class CommonErrorControllerAdvice {
     @ExceptionHandler
     @ResponseStatus(HttpStatus.BAD_REQUEST)
     public Mono<ResponseMessage<Object>> handleException(IllegalArgumentException e) {
-        return Mono.just(ResponseMessage.error(400, "illegal_argument", e.getMessage()))
-                   .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getMessage(), e)));
+
+        return LocaleUtils
+                .resolveThrowable(messageSource, e, (err, msg) -> ResponseMessage.error(400, "illegal_argument", msg))
+                .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getMessage(), e)))
+                ;
     }
 
     @ExceptionHandler
     @ResponseStatus(HttpStatus.BAD_REQUEST)
     public Mono<ResponseMessage<Object>> handleException(AuthenticationException e) {
-        return Mono.just(ResponseMessage.error(400, e.getCode(), e.getMessage()));
+        return LocaleUtils
+                .resolveThrowable(messageSource, e, (err, msg) -> ResponseMessage.error(400, e.getCode(), msg))
+                .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getMessage(), e)))
+                ;
     }
 
     @ExceptionHandler
     @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
-    public Mono<ResponseMessage<Object>> handleException(MediaTypeNotSupportedStatusException e) {
-        return Mono.just(ResponseMessage
-                                 .error(415, "unsupported_media_type", "不支持的请求类型")
-                                 .result(e.getSupportedMediaTypes()))
-                   .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getMessage(), e)));
+    public Mono<ResponseMessage<Object>> handleException(UnsupportedMediaTypeStatusException e) {
+        return LocaleUtils
+                .resolveMessage(messageSource, "error.unsupported_media_type")
+                .map(msg -> ResponseMessage
+                        .error(415, "unsupported_media_type", msg)
+                        .result(e.getSupportedMediaTypes()))
+                .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getMessage(), e)));
     }
 
     @ExceptionHandler
     @ResponseStatus(HttpStatus.NOT_ACCEPTABLE)
     public Mono<ResponseMessage<Object>> handleException(NotAcceptableStatusException e) {
-        return Mono.just(ResponseMessage
-                                 .error(406, "not_acceptable_media_type", "不支持的响应类型")
-                                 .result(e.getSupportedMediaTypes()))
-                   .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getMessage(), e)));
+
+        return LocaleUtils
+                .resolveMessage(messageSource, "error.not_acceptable_media_type")
+                .map(msg -> ResponseMessage
+                        .error(406, "not_acceptable_media_type", msg)
+                        .result(e.getSupportedMediaTypes()))
+                .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getMessage(), e)));
     }
 
     @ExceptionHandler
     @ResponseStatus(HttpStatus.NOT_ACCEPTABLE)
     public Mono<ResponseMessage<Object>> handleException(MethodNotAllowedException e) {
-        return Mono.just(ResponseMessage
-                                 .error(405, "method_not_allowed", "不支持的请求方法:" + e.getHttpMethod())
-                                 .result(e.getSupportedMethods()))
-                   .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getMessage(), e)));
+        return LocaleUtils
+                .resolveMessage(messageSource, "error.method_not_allowed")
+                .map(msg -> ResponseMessage
+                        .error(406, "method_not_allowed", msg)
+                        .result(e.getSupportedMethods()))
+                .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getMessage(), e)));
     }
 
     @ExceptionHandler
     @ResponseStatus(HttpStatus.BAD_REQUEST)
-    public Mono<ResponseMessage<Object>> handleException(R2dbcDataIntegrityViolationException exception) {
-        if (exception.getMessage().contains("Duplicate")) {
-            return Mono.just(ResponseMessage.error("存在重复的数据"));
+    public Mono<ResponseMessage<Object>> handleException(R2dbcDataIntegrityViolationException e) {
+        String code;
+
+        if (e.getMessage().contains("Duplicate")) {
+            code = "error.duplicate_data";
+        } else {
+            code = "error.data_error";
+            log.warn(e.getMessage(), e);
         }
-        log.warn(exception.getMessage(), exception);
-        return Mono.just(ResponseMessage.error("数据错误"));
+        return LocaleUtils
+                .resolveMessage(messageSource, code)
+                .map(msg -> ResponseMessage.error(400, code, msg));
     }
 
 
@@ -223,7 +245,10 @@ public class CommonErrorControllerAdvice {
 
         } while (exception != null && exception != e);
 
-        return Mono.just(ResponseMessage.error(400, "illegal_argument", e.getMessage()));
+        return LocaleUtils
+                .resolveThrowable(messageSource,
+                                  exception,
+                                  (err, msg) -> ResponseMessage.error(400, "illegal_argument", msg));
     }
 
 }

+ 2 - 7
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonWebFluxConfiguration.java

@@ -10,12 +10,14 @@ import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.i18n.LocaleContext;
 import org.springframework.context.support.ReloadableResourceBundleMessageSource;
+import org.springframework.context.support.ResourceBundleMessageSource;
 import org.springframework.core.ReactiveAdapterRegistry;
 import org.springframework.http.codec.ServerCodecConfigurer;
 import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
 import org.springframework.web.server.ServerWebExchange;
 import org.springframework.web.server.WebFilter;
 import org.springframework.web.server.WebFilterChain;
+import org.springframework.web.server.i18n.LocaleContextResolver;
 import reactor.core.publisher.Mono;
 
 @Configuration
@@ -38,13 +40,6 @@ public class CommonWebFluxConfiguration {
         return new ResponseMessageWrapper(codecConfigurer.getWriters(), resolver, registry);
     }
 
-    @Bean
-    public MessageSource messageSource() {
-        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
-        messageSource.setBasenames("i18n/messages");
-        messageSource.setDefaultEncoding("UTF-8");
-        return messageSource;
-    }
 
     @Bean
     public WebFilter localeWebFilter() {

+ 2 - 1
hsweb-commons/hsweb-commons-crud/src/main/resources/META-INF/spring.factories

@@ -3,4 +3,5 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
 org.hswebframework.web.crud.configuration.EasyormConfiguration,\
 org.hswebframework.web.crud.configuration.JdbcSqlExecutorConfiguration,\
 org.hswebframework.web.crud.configuration.R2dbcSqlExecutorConfiguration,\
-org.hswebframework.web.crud.web.CommonWebFluxConfiguration
+org.hswebframework.web.crud.web.CommonWebFluxConfiguration,\
+org.hswebframework.web.crud.configuration.Commons18nConfiguration

+ 5 - 0
hsweb-commons/hsweb-commons-crud/src/main/resources/i18n/commons/messages_en_US.properties

@@ -0,0 +1,5 @@
+error.unsupported_media_type=Unsupported media type
+error.not_acceptable_media_type=Not acceptable media type
+error.method_not_allowed=Method not allowed
+error.duplicate_data=Duplicate data
+error.data_error=Data error

+ 5 - 0
hsweb-commons/hsweb-commons-crud/src/main/resources/i18n/commons/messages_zh_CN.properties

@@ -0,0 +1,5 @@
+error.unsupported_media_type=不支持的请求类型
+error.not_acceptable_media_type=不支持的响应类型
+error.method_not_allowed=不支持的请求方法
+error.duplicate_data=重复的数据
+error.data_error=数据错误

+ 27 - 22
hsweb-core/src/main/java/org/hswebframework/web/dict/EnumDict.java

@@ -127,7 +127,6 @@ public interface EnumDict<V> extends JSONSerializable {
     }
 
 
-
     /**
      * 从指定的枚举类中查找想要的枚举,并返回一个{@link Optional},如果未找到,则返回一个{@link Optional#empty()}
      *
@@ -150,8 +149,8 @@ public interface EnumDict<V> extends JSONSerializable {
     static <T extends Enum & EnumDict> List<T> findList(Class<T> type, Predicate<T> predicate) {
         if (type.isEnum()) {
             return Arrays.stream(type.getEnumConstants())
-                    .filter(predicate)
-                    .collect(Collectors.toList());
+                         .filter(predicate)
+                         .collect(Collectors.toList());
         }
         return Collections.emptyList();
     }
@@ -162,7 +161,9 @@ public interface EnumDict<V> extends JSONSerializable {
      * @see this#find(Class, Predicate)
      */
     static <T extends Enum & EnumDict<?>> Optional<T> findByValue(Class<T> type, Object value) {
-        return find(type, e -> e.getValue() == value || e.getValue().equals(value) || String.valueOf(e.getValue()).equalsIgnoreCase(String.valueOf(value)));
+        return find(type, e -> e.getValue() == value || e.getValue().equals(value) || String
+                .valueOf(e.getValue())
+                .equalsIgnoreCase(String.valueOf(value)));
     }
 
     /**
@@ -203,8 +204,8 @@ public interface EnumDict<V> extends JSONSerializable {
         if (all.length >= 64) {
             List<T> list = Arrays.asList(t);
             return Arrays.stream(all)
-                    .map(EnumDict.class::cast)
-                    .anyMatch(list::contains);
+                         .map(EnumDict.class::cast)
+                         .anyMatch(list::contains);
         }
         return maskIn(toMask(t), target);
     }
@@ -295,7 +296,7 @@ public interface EnumDict<V> extends JSONSerializable {
     @AllArgsConstructor
     @NoArgsConstructor
     class EnumDictJSONDeserializer extends JsonDeserializer implements ObjectDeserializer {
-        private Function<Object,Object> mapper;
+        private Function<Object, Object> mapper;
 
         @Override
         @SuppressWarnings("all")
@@ -324,8 +325,10 @@ public interface EnumDict<V> extends JSONSerializable {
                     value = parser.parse();
                     if (value instanceof Map) {
                         return (T) EnumDict.find(((Class) type), ((Map) value).get("value"))
-                                .orElseGet(() ->
-                                        EnumDict.find(((Class) type), ((Map) value).get("text")).orElse(null));
+                                           .orElseGet(() ->
+                                                              EnumDict
+                                                                      .find(((Class) type), ((Map) value).get("text"))
+                                                                      .orElse(null));
                     }
                 }
 
@@ -347,11 +350,11 @@ public interface EnumDict<V> extends JSONSerializable {
         @SneakyThrows
         public Object deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
             JsonNode node = jp.getCodec().readTree(jp);
-            if(mapper!=null){
-                if(node.isTextual()){
+            if (mapper != null) {
+                if (node.isTextual()) {
                     return mapper.apply(node.asText());
                 }
-                if(node.isNumber()){
+                if (node.isNumber()) {
                     return mapper.apply(node.asLong());
                 }
             }
@@ -364,19 +367,20 @@ public interface EnumDict<V> extends JSONSerializable {
                 findPropertyType = BeanUtils.findPropertyType(currentName, currentValue.getClass());
             }
             Supplier<ValidationException> exceptionSupplier = () -> {
-               List<Object> values= Stream.of(findPropertyType.getEnumConstants())
+                List<Object> values = Stream
+                        .of(findPropertyType.getEnumConstants())
                         .map(Enum.class::cast)
-                        .map(e->{
-                            if(e instanceof EnumDict){
+                        .map(e -> {
+                            if (e instanceof EnumDict) {
                                 return ((EnumDict) e).getValue();
                             }
                             return e.name();
                         }).collect(Collectors.toList());
 
-                return new ValidationException("参数[" + currentName + "]在选项中不存在",
-                        Arrays.asList(
-                                new ValidationException.Detail(currentName, "选项中不存在此值", values)
-                        ));
+                return new ValidationException("validation.parameter_does_not_exist_in_enums",
+                                               Arrays.asList(
+                                                       new ValidationException.Detail(currentName, "选项中不存在此值", values)
+                                               ), currentName);
             };
             if (EnumDict.class.isAssignableFrom(findPropertyType) && findPropertyType.isEnum()) {
                 if (node.isObject()) {
@@ -394,12 +398,13 @@ public interface EnumDict<V> extends JSONSerializable {
                             .find(findPropertyType, node.textValue())
                             .orElseThrow(exceptionSupplier);
                 }
-                throw new ValidationException("参数[" + currentName + "]在选项中不存在", Arrays.asList(
+                throw new ValidationException("validation.parameter_does_not_exist_in_enums", Arrays.asList(
                         new ValidationException.Detail(currentName, "选项中不存在此值", null)
-                ));
+                ), currentName);
             }
             if (findPropertyType.isEnum()) {
-                return Stream.of(findPropertyType.getEnumConstants())
+                return Stream
+                        .of(findPropertyType.getEnumConstants())
                         .filter(o -> {
                             if (node.isTextual()) {
                                 return node.textValue().equalsIgnoreCase(((Enum) o).name());

+ 0 - 4
hsweb-core/src/main/java/org/hswebframework/web/exception/BusinessException.java

@@ -43,10 +43,6 @@ public class BusinessException extends I18nSupportException {
         this(message, null, status, args);
     }
 
-    public BusinessException(String message, String code, Object... args) {
-        this(message, code, 500, args);
-    }
-
     public BusinessException(String message, String code, int status, Object... args) {
         super(message, args);
         this.code = code;

+ 2 - 2
hsweb-core/src/main/java/org/hswebframework/web/exception/ValidationException.java

@@ -28,8 +28,8 @@ public class ValidationException extends BusinessException {
         this(message, Collections.singletonList(new Detail(property, message, null)));
     }
 
-    public ValidationException(String message, List<Detail> details) {
-        super(message);
+    public ValidationException(String message, List<Detail> details,Object... args) {
+        super(message,400,args);
         this.details = details;
     }
 

+ 3 - 3
hsweb-core/src/main/java/org/hswebframework/web/i18n/LocaleUtils.java

@@ -49,9 +49,9 @@ public class LocaleUtils {
                 });
     }
 
-    public static Mono<String> reactiveMessage(MessageSource messageSource,
-                                               String code,
-                                               Object... args) {
+    public static Mono<String> resolveMessage(MessageSource messageSource,
+                                              String code,
+                                              Object... args) {
         return reactive()
                 .map(ctx -> resolveMessage(messageSource, code, ctx.getLocale(), code, args));
     }

+ 16 - 1
hsweb-core/src/main/java/org/hswebframework/web/i18n/WebFluxLocaleFilter.java

@@ -1,18 +1,33 @@
 package org.hswebframework.web.i18n;
 
 import org.springframework.context.i18n.LocaleContext;
+import org.springframework.context.i18n.SimpleLocaleContext;
 import org.springframework.lang.NonNull;
+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;
 
+import java.util.Locale;
+import java.util.Optional;
+
 public class WebFluxLocaleFilter implements WebFilter {
     @Override
     @NonNull
     public Mono<Void> filter(@NonNull ServerWebExchange exchange, WebFilterChain chain) {
         return chain
                 .filter(exchange)
-                .subscriberContext(ctx -> ctx.put(LocaleContext.class, exchange.getLocaleContext()));
+                .subscriberContext(ctx -> ctx.put(LocaleContext.class, getLocaleContext(exchange)));
+    }
+
+    public LocaleContext getLocaleContext(ServerWebExchange exchange) {
+        String lang = exchange.getRequest()
+                              .getQueryParams()
+                              .getFirst(":lang");
+        if (StringUtils.hasText(lang)) {
+            return new SimpleLocaleContext(Locale.forLanguageTag(lang));
+        }
+        return exchange.getLocaleContext();
     }
 }

+ 3 - 0
hsweb-core/src/main/resources/i18n/core/messages_en_US.properties

@@ -0,0 +1,3 @@
+error.not_found=The data does not exist
+error.cant_create_instance=Unable to create instance:{0}
+validation.parameter_does_not_exist_in_enums=Parameter {0} does not exist in option

+ 4 - 0
hsweb-core/src/main/resources/i18n/core/messages_zh_CN.properties

@@ -0,0 +1,4 @@
+error.not_found=数据不存在
+error.cant_create_instance=无法创建实例:{0}
+
+validation.parameter_does_not_exist_in_enums=参数[{0}]在选择中不存在

+ 0 - 2
hsweb-core/src/main/resources/i18n/messages_zh_CN.properties

@@ -1,2 +0,0 @@
-error.not_found=数据不存在
-error.cant_create_instance=无法创建实例:{0}

+ 70 - 0
hsweb-starter/src/main/java/org/hswebframework/web/starter/i18n/CompositeMessageSource.java

@@ -0,0 +1,70 @@
+package org.hswebframework.web.starter.i18n;
+
+import org.springframework.context.MessageSource;
+import org.springframework.context.MessageSourceResolvable;
+import org.springframework.context.NoSuchMessageException;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.StringUtils;
+
+import javax.annotation.Nonnull;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+public class CompositeMessageSource implements MessageSource {
+
+    private final List<MessageSource> messageSources = new CopyOnWriteArrayList<>();
+
+    public void addMessageSources(Collection<MessageSource> source) {
+        messageSources.addAll(source);
+    }
+
+    public void addMessageSource(MessageSource source) {
+        messageSources.add(source);
+    }
+
+    @Override
+    public String getMessage(@Nonnull String code, Object[] args, String defaultMessage, @Nonnull Locale locale) {
+        for (MessageSource messageSource : messageSources) {
+            String result = messageSource.getMessage(code, args, defaultMessage, locale);
+            if (StringUtils.hasText(result)) {
+                return result;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    @Nonnull
+    public String getMessage(@Nonnull String code, Object[] args, @Nonnull Locale locale) throws NoSuchMessageException {
+        for (MessageSource messageSource : messageSources) {
+            try {
+                String result = messageSource.getMessage(code, args, locale);
+                if (StringUtils.hasText(result)) {
+                    return result;
+                }
+            } catch (NoSuchMessageException ignore) {
+
+            }
+        }
+        throw new NoSuchMessageException(code, locale);
+    }
+
+    @Override
+    @Nonnull
+    public String getMessage(@Nonnull MessageSourceResolvable resolvable, @Nonnull Locale locale) throws NoSuchMessageException {
+        for (MessageSource messageSource : messageSources) {
+            try {
+                String result = messageSource.getMessage(resolvable, locale);
+                if (StringUtils.hasText(result)) {
+                    return result;
+                }
+            } catch (NoSuchMessageException ignore) {
+
+            }
+        }
+        String[] codes = resolvable.getCodes();
+        throw new NoSuchMessageException(!ObjectUtils.isEmpty(codes) ? codes[codes.length - 1] : "", locale);
+    }
+}

+ 38 - 0
hsweb-starter/src/main/java/org/hswebframework/web/starter/i18n/I18nConfiguration.java

@@ -0,0 +1,38 @@
+package org.hswebframework.web.starter.i18n;
+
+import org.hswebframework.web.exception.BusinessException;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.AutoConfigureOrder;
+import org.springframework.context.MessageSource;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+import org.springframework.context.support.ResourceBundleMessageSource;
+import org.springframework.core.Ordered;
+
+import java.util.stream.Collectors;
+
+@Configuration(proxyBeanMethods = false)
+@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
+public class I18nConfiguration {
+
+
+    @Bean
+    public MessageSource coreMessageSource(){
+        ResourceBundleMessageSource messageSource=new ResourceBundleMessageSource();
+        messageSource.setDefaultEncoding("UTF-8");
+        messageSource.setBundleClassLoader(BusinessException.class.getClassLoader());
+        messageSource.setBasenames("i18n/core/messages");
+        return messageSource;
+    }
+
+    @Bean
+    @Primary
+    public MessageSource compositeMessageSource(ObjectProvider<MessageSource> objectProvider) {
+        CompositeMessageSource messageSource = new CompositeMessageSource();
+        messageSource.addMessageSources(objectProvider.stream().collect(Collectors.toList()));
+        return messageSource;
+    }
+
+
+}

+ 2 - 1
hsweb-starter/src/main/resources/META-INF/spring.factories

@@ -2,4 +2,5 @@
 org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
 org.hswebframework.web.starter.jackson.CustomCodecsAutoConfiguration,\
 org.hswebframework.web.starter.HswebAutoConfiguration,\
-org.hswebframework.web.starter.CorsAutoConfiguration
+org.hswebframework.web.starter.CorsAutoConfiguration,\
+org.hswebframework.web.starter.i18n.I18nConfiguration