From 776ddd28c1e2019977d0ec2c7e3dd18da1d13aa9 Mon Sep 17 00:00:00 2001 From: siujamo Date: Tue, 24 Mar 2026 10:49:33 +0800 Subject: [PATCH] feat: implement internationalisation support with message utility and localisation configuration --- .../com/onixbyte/helix/config/I18nConfig.java | 41 ++++++++++++++++ .../helix/config/ValidationConfig.java | 47 +++++++++++++++++++ .../helix/controller/AssetController.java | 16 ++++--- .../helix/controller/AuthorityController.java | 11 ++++- .../helix/controller/UserController.java | 19 +++++--- .../web/request/ResetPasswordRequest.java | 1 - .../onixbyte/helix/manager/UserManager.java | 4 +- .../onixbyte/helix/service/UserService.java | 4 +- .../com/onixbyte/helix/shared/Message.java | 33 +++++++++++++ .../com/onixbyte/helix/utils/MessageUtil.java | 42 +++++++++++++++++ src/main/resources/i18n/messages.properties | 28 +++++++++++ .../resources/i18n/messages_en_GB.properties | 28 +++++++++++ .../resources/i18n/messages_zh_CN.properties | 28 +++++++++++ 13 files changed, 282 insertions(+), 20 deletions(-) create mode 100644 src/main/java/com/onixbyte/helix/config/I18nConfig.java create mode 100644 src/main/java/com/onixbyte/helix/config/ValidationConfig.java create mode 100644 src/main/java/com/onixbyte/helix/shared/Message.java create mode 100644 src/main/java/com/onixbyte/helix/utils/MessageUtil.java create mode 100644 src/main/resources/i18n/messages.properties create mode 100644 src/main/resources/i18n/messages_en_GB.properties create mode 100644 src/main/resources/i18n/messages_zh_CN.properties diff --git a/src/main/java/com/onixbyte/helix/config/I18nConfig.java b/src/main/java/com/onixbyte/helix/config/I18nConfig.java new file mode 100644 index 0000000..a6a0322 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/config/I18nConfig.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024-2026 OnixByte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.onixbyte.helix.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.LocaleResolver; +import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver; + +import java.util.Locale; + +@Configuration +public class I18nConfig { + + @Bean + public LocaleResolver localeResolver() { + var slr = new AcceptHeaderLocaleResolver(); + slr.setDefaultLocale(Locale.UK); + return slr; + } +} diff --git a/src/main/java/com/onixbyte/helix/config/ValidationConfig.java b/src/main/java/com/onixbyte/helix/config/ValidationConfig.java new file mode 100644 index 0000000..4462d47 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/config/ValidationConfig.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024-2026 OnixByte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.onixbyte.helix.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; + +@Configuration +public class ValidationConfig { + + private final MessageSource messageSource; + + @Autowired + public ValidationConfig(MessageSource messageSource) { + this.messageSource = messageSource; + } + + @Bean + public LocalValidatorFactoryBean getValidator() { + var factoryBean = new LocalValidatorFactoryBean(); + factoryBean.setValidationMessageSource(messageSource); + return factoryBean; + } +} diff --git a/src/main/java/com/onixbyte/helix/controller/AssetController.java b/src/main/java/com/onixbyte/helix/controller/AssetController.java index c413f38..270bfeb 100644 --- a/src/main/java/com/onixbyte/helix/controller/AssetController.java +++ b/src/main/java/com/onixbyte/helix/controller/AssetController.java @@ -4,6 +4,8 @@ import com.onixbyte.helix.shared.AssetPrefix; import com.onixbyte.helix.domain.web.response.FileUploadResponse; import com.onixbyte.helix.exception.BizException; import com.onixbyte.helix.service.AssetService; +import com.onixbyte.helix.shared.Message; +import com.onixbyte.helix.utils.MessageUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -25,15 +27,15 @@ public class AssetController { private static final Logger log = LoggerFactory.getLogger(AssetController.class); private final AssetService assetService; + private final MessageUtil messageUtil; - /** - * Constructs a new FileController with the specified file service. - * - * @param assetService the file service to use for file operations - */ @Autowired - public AssetController(AssetService assetService) { + public AssetController( + AssetService assetService, + MessageUtil messageUtil + ) { this.assetService = assetService; + this.messageUtil = messageUtil; } /** @@ -48,7 +50,7 @@ public class AssetController { ) { try { if (file.isEmpty()) { - throw new BizException(HttpStatus.BAD_REQUEST, "File cannot be empty."); + throw new BizException(HttpStatus.BAD_REQUEST, messageUtil.getMessage(Message.ASSET_NOT_EMPTY)); } var fileUrl = assetService.uploadFile(AssetPrefix.UPLOADS, file); diff --git a/src/main/java/com/onixbyte/helix/controller/AuthorityController.java b/src/main/java/com/onixbyte/helix/controller/AuthorityController.java index 01071af..ee5dd4f 100644 --- a/src/main/java/com/onixbyte/helix/controller/AuthorityController.java +++ b/src/main/java/com/onixbyte/helix/controller/AuthorityController.java @@ -5,6 +5,8 @@ import com.onixbyte.helix.domain.web.request.AuthorityRequest; import com.onixbyte.helix.domain.web.request.QueryAuthorityRequest; import com.onixbyte.helix.domain.web.response.ActionResponse; import com.onixbyte.helix.service.AuthorityService; +import com.onixbyte.helix.shared.Message; +import com.onixbyte.helix.utils.MessageUtil; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.validation.annotation.Validated; @@ -21,9 +23,14 @@ import org.springframework.web.bind.annotation.*; public class AuthorityController { private final AuthorityService authorityService; + private final MessageUtil messageUtil; - public AuthorityController(AuthorityService authorityService) { + public AuthorityController( + AuthorityService authorityService, + MessageUtil messageUtil + ) { this.authorityService = authorityService; + this.messageUtil = messageUtil; } /** @@ -72,6 +79,6 @@ public class AuthorityController { @DeleteMapping("/{authorityId:\\d+}") public ActionResponse deleteAuthority(@PathVariable Long authorityId) { var name = authorityService.deleteAuthority(authorityId); - return ActionResponse.success("Authority [%s] deleted.".formatted(name)); + return ActionResponse.success(messageUtil.getMessage(Message.AUTHORITY_DELETED, name)); } } diff --git a/src/main/java/com/onixbyte/helix/controller/UserController.java b/src/main/java/com/onixbyte/helix/controller/UserController.java index 642d31f..a2bf304 100644 --- a/src/main/java/com/onixbyte/helix/controller/UserController.java +++ b/src/main/java/com/onixbyte/helix/controller/UserController.java @@ -7,6 +7,8 @@ import com.onixbyte.helix.domain.web.request.ResetPasswordRequest; import com.onixbyte.helix.domain.web.response.ActionResponse; import com.onixbyte.helix.domain.web.response.UserDetailResponse; import com.onixbyte.helix.service.UserService; +import com.onixbyte.helix.shared.Message; +import com.onixbyte.helix.utils.MessageUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -27,10 +29,12 @@ import org.springframework.web.bind.annotation.*; public class UserController { private final UserService userService; + private final MessageUtil messageUtil; @Autowired - public UserController(UserService userService) { + public UserController(UserService userService, MessageUtil messageUtil) { this.userService = userService; + this.messageUtil = messageUtil; } /** @@ -94,10 +98,13 @@ public class UserController { * @return action response */ @PreAuthorize("hasAnyAuthority('system:user:reset-password')") - @PatchMapping("/reset-password") - public ActionResponse resetPassword(@Validated @RequestBody ResetPasswordRequest request) { - userService.resetPassword(request); - return ActionResponse.success("密码修改成功"); + @PatchMapping("/reset-password/{id:\\d+}") + public ActionResponse resetPassword( + @PathVariable Long id, + @Validated @RequestBody ResetPasswordRequest request + ) { + userService.resetPassword(id, request); + return ActionResponse.success(messageUtil.getMessage(Message.USER_PASSWORD_RESET_SUCCESS)); } /** @@ -110,6 +117,6 @@ public class UserController { @DeleteMapping("/{userId:\\d+}") public ActionResponse deleteUser(@PathVariable Long userId) { userService.deleteUser(userId); - return ActionResponse.success("删除成功"); + return ActionResponse.success(messageUtil.getMessage(Message.USER_DELETED)); } } diff --git a/src/main/java/com/onixbyte/helix/domain/web/request/ResetPasswordRequest.java b/src/main/java/com/onixbyte/helix/domain/web/request/ResetPasswordRequest.java index 2ee0a27..d1ac2d3 100644 --- a/src/main/java/com/onixbyte/helix/domain/web/request/ResetPasswordRequest.java +++ b/src/main/java/com/onixbyte/helix/domain/web/request/ResetPasswordRequest.java @@ -4,7 +4,6 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; public record ResetPasswordRequest( - @NotNull(message = "用户 ID 不能为空") Long id, @NotBlank(message = "密码不能为空") String password ) { } diff --git a/src/main/java/com/onixbyte/helix/manager/UserManager.java b/src/main/java/com/onixbyte/helix/manager/UserManager.java index 4c986b3..3b19dee 100644 --- a/src/main/java/com/onixbyte/helix/manager/UserManager.java +++ b/src/main/java/com/onixbyte/helix/manager/UserManager.java @@ -150,9 +150,9 @@ public class UserManager { } @Transactional(rollbackFor = Throwable.class) - public void updateUserPassword(ResetPasswordRequest request) { + public void updatePasswordById(Long id, ResetPasswordRequest request) { userCredentialMapper.updateUserCredential( - request.id(), + id, passwordEncoder.encode(request.password()) ); } diff --git a/src/main/java/com/onixbyte/helix/service/UserService.java b/src/main/java/com/onixbyte/helix/service/UserService.java index 42fcf65..adda0b7 100644 --- a/src/main/java/com/onixbyte/helix/service/UserService.java +++ b/src/main/java/com/onixbyte/helix/service/UserService.java @@ -162,8 +162,8 @@ public class UserService { } @Transactional(rollbackFor = Throwable.class) - public void resetPassword(ResetPasswordRequest request) { - userManager.updateUserPassword(request); + public void resetPassword(Long id, ResetPasswordRequest request) { + userManager.updatePasswordById(id, request); } @Transactional(rollbackFor = Throwable.class) diff --git a/src/main/java/com/onixbyte/helix/shared/Message.java b/src/main/java/com/onixbyte/helix/shared/Message.java new file mode 100644 index 0000000..0111874 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/shared/Message.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024-2026 OnixByte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.onixbyte.helix.shared; + +public class Message { + + public static final String ASSET_NOT_EMPTY = "asset.not-empty"; + + public static final String AUTHORITY_DELETED = "authority.deleted"; + + public static final String USER_PASSWORD_RESET_SUCCESS = "user.password-reset-success"; + public static final String USER_DELETED = "user.deleted"; +} diff --git a/src/main/java/com/onixbyte/helix/utils/MessageUtil.java b/src/main/java/com/onixbyte/helix/utils/MessageUtil.java new file mode 100644 index 0000000..9e9ede8 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/utils/MessageUtil.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024-2026 OnixByte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.onixbyte.helix.utils; + +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.stereotype.Component; + +@Component +public class MessageUtil { + + private final MessageSource messageSource; + + public MessageUtil(MessageSource messageSource) { + this.messageSource = messageSource; + } + + public String getMessage(String code, Object... args) { + var locale = LocaleContextHolder.getLocale(); + return messageSource.getMessage(code, args, locale); + } +} diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties new file mode 100644 index 0000000..f9a18f6 --- /dev/null +++ b/src/main/resources/i18n/messages.properties @@ -0,0 +1,28 @@ +# +# Copyright (c) 2024-2026 OnixByte +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +asset.not-empty = File cannot be empty. + +authority.deleted = Authority [{0}] deleted. + +user.password-reset-success = Password has been reset. +user.deleted = User [{0}] deleted. \ No newline at end of file diff --git a/src/main/resources/i18n/messages_en_GB.properties b/src/main/resources/i18n/messages_en_GB.properties new file mode 100644 index 0000000..f9a18f6 --- /dev/null +++ b/src/main/resources/i18n/messages_en_GB.properties @@ -0,0 +1,28 @@ +# +# Copyright (c) 2024-2026 OnixByte +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +asset.not-empty = File cannot be empty. + +authority.deleted = Authority [{0}] deleted. + +user.password-reset-success = Password has been reset. +user.deleted = User [{0}] deleted. \ No newline at end of file diff --git a/src/main/resources/i18n/messages_zh_CN.properties b/src/main/resources/i18n/messages_zh_CN.properties new file mode 100644 index 0000000..009ffa2 --- /dev/null +++ b/src/main/resources/i18n/messages_zh_CN.properties @@ -0,0 +1,28 @@ +# +# Copyright (c) 2024-2026 OnixByte +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +asset.not-empty = 文件不能为空。 + +authority.deleted = 权限【{0}】删除成功。 + +user.password-reset-success = 密码修改成功。 +user.deleted = 用户【{0}】删除成功。 \ No newline at end of file