feat(i18n): Add international support module

- Added I18nProperties configuration class to define international related properties
- Implement I18nAutoConfiguration automatic configuration class, providing international message sources and local resolvers
- Added the I18nWebConfiguration configuration class to register language switching interceptors
- Added InternalInformation and ResponseInformation interfaces to extend and provide international information to the outside world
- Implement I18nBaseException class to handle internationalization-related exceptions
- Add ReactiveLocaleResolver filter to parse language information in WebFlux requests
- Create a WebFluxLocaleResolver interface to define a local resolver for WebFlux type
- Updated project
This commit is contained in:
gewuyou 2025-04-27 18:10:31 +08:00
parent fc8bea06b3
commit e349dc785d
14 changed files with 428 additions and 0 deletions

3
forgeboot-i18n/.gitattributes vendored Normal file
View File

@ -0,0 +1,3 @@
/gradlew text eol=lf
*.bat text eol=crlf
*.jar binary

40
forgeboot-i18n/.gitignore vendored Normal file
View File

@ -0,0 +1,40 @@
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Kotlin ###
.kotlin

View File

@ -0,0 +1,11 @@
extra {
// 需要SpringBootBom
setProperty(ProjectFlags.USE_SPRING_BOOT_BOM, true)
}
dependencies {
implementation(project(Modules.Core.EXTENSION))
compileOnly(libs.springBootStarter.web)
// Spring Boot WebFlux
compileOnly(libs.springBootStarter.webflux)
}

View File

@ -0,0 +1,155 @@
package com.gewuyou.forgeboot.i18n.config
import com.gewuyou.forgeboot.i18n.filter.ReactiveLocaleResolver
import com.gewuyou.forgeboot.core.extension.log
import com.gewuyou.forgeboot.i18n.config.entity.I18nProperties
import jakarta.servlet.http.HttpServletRequest
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.MessageSource
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.support.ReloadableResourceBundleMessageSource
import org.springframework.core.io.support.PathMatchingResourcePatternResolver
import org.springframework.util.StringUtils
import org.springframework.web.servlet.LocaleResolver
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor
import java.io.IOException
import java.util.*
/**
* 本地化配置
*
* @author gewuyou
* @since 2024-11-11 00:46:01
*/
@Configuration
@EnableConfigurationProperties(I18nProperties::class)
open class I18nAutoConfiguration (
private val i18nProperties: I18nProperties
){
/**
* 配置并创建一个国际化的消息源
*
* 此方法首先会扫描指定路径下所有的国际化属性文件然后将这些文件路径设置到消息源中
* 如果项目中还没有名为 [MESSAGE_SOURCE_BEAN_NAME] 的消息源 bean则会创建一个
*
* @return MessageSource 国际化消息源
*/
@Bean(name = [MESSAGE_SOURCE_BEAN_NAME])
@ConditionalOnMissingBean(name = [MESSAGE_SOURCE_BEAN_NAME])
open fun messageSource(): MessageSource {
log.info("开始加载 I18n 配置...")
val messageSource = ReloadableResourceBundleMessageSource()
// 动态扫描所有 i18n 子目录下的 messages.properties 文件
val baseNames = scanBaseNames(i18nProperties.wildPathForLanguageFiles)
// 设置文件路径到 messageSource
messageSource.setBasenames(*baseNames.toTypedArray<String>())
messageSource.setDefaultEncoding("UTF-8")
log.info("I18n 配置加载完成...")
return messageSource
}
/**
* 扫描指定路径下的所有国际化属性文件路径
*
* 此方法会根据提供的基础路径查找所有匹配的国际化属性文件并将其路径添加到列表中返回
* 主要用于动态加载项目中的国际化配置文件
*
* @param basePath 国际化属性文件所在的基路径
* @return List<String> 包含所有找到的国际化属性文件路径的列表
*/
private fun scanBaseNames(basePath: String): List<String> {
val baseNames: MutableList<String> = ArrayList()
log.info("开始扫描 I18n 文件 {}", basePath)
try {
val resources = PathMatchingResourcePatternResolver().getResources(
"$basePath*.properties"
)
for (resource in resources) {
val path = resource.uri.toString()
log.info("找到 I18n 文件路径: {}", path)
// 转换路径为 Spring 的 basename 格式(去掉 .properties 后缀)
val baseName = path.substring(0, path.lastIndexOf(".properties"))
if (!baseNames.contains(baseName)) {
baseNames.add(baseName)
}
}
} catch (e: IOException) {
log.error("无法扫描 I18n 文件", e)
}
return baseNames
}
/**
* 配置并创建一个区域设置解析器
*
* 此方法创建一个自定义的区域设置解析器它首先尝试从请求参数中解析语言设置
* 如果参数不存在则使用请求头中的语言设置
*
* @param i18nProperties 本地化属性配置
* @return LocaleResolver 区域设置解析器
*/
@Bean
@ConditionalOnClass(name = ["org.springframework.web.servlet.DispatcherServlet"])
open fun localeResolver(i18nProperties: I18nProperties): LocaleResolver {
return object : AcceptHeaderLocaleResolver() {
override fun resolveLocale(request: HttpServletRequest): Locale {
log.info("开始解析URL参数 lang 语言配置...")
// 先检查URL参数 ?lang=xx
val lang = request.getParameter(i18nProperties.langRequestParameter)
if (StringUtils.hasText(lang)) {
return Locale.forLanguageTag(lang)
}
// 设置默认语言为简体中文
this.defaultLocale = Locale.forLanguageTag(i18nProperties.defaultLocale)
// 返回请求头 Accept-Language 的语言配置
return super.resolveLocale(request)
}
}
}
/**
* 创建一个区域设置更改拦截器
*
* 此方法在项目为 Servlet 类型且配置了相应的属性时被调用创建的拦截器用于处理 URL 参数中的语言变更请求
*
* @param i18nProperties 本地化属性配置
* @return LocaleChangeInterceptor 区域设置更改拦截器
*/
@Bean
@ConditionalOnProperty(name = ["spring.main.web-application-type"], havingValue = "servlet", matchIfMissing = true)
open fun localeChangeInterceptor(i18nProperties: I18nProperties): LocaleChangeInterceptor {
log.info("创建区域设置更改拦截器...")
val interceptor = LocaleChangeInterceptor()
// 设置 URL 参数名,例如 ?lang=en 或 ?lang=zh
interceptor.paramName = i18nProperties.langRequestParameter
return interceptor
}
/**
* 创建一个适用于 WebFlux 的区域设置解析器
*
* 当项目配置为 Reactive 类型时此方法会被调用用于创建一个 Reactive 类型的区域设置解析器
*
* @param i18nProperties 本地化属性配置
* @return ReactiveLocaleResolver 适用于 WebFlux 的区域设置解析器
*/
@Bean
@ConditionalOnProperty(name = ["spring.main.web-application-type"], havingValue = "reactive")
open fun createReactiveLocaleResolver(i18nProperties: I18nProperties): ReactiveLocaleResolver {
log.info("创建 WebFlux 区域设置解析器...")
return ReactiveLocaleResolver(i18nProperties)
}
companion object {
/**
* 消息源 bean 的名称
*/
const val MESSAGE_SOURCE_BEAN_NAME: String = "i18nMessageSource"
}
}

View File

@ -0,0 +1,35 @@
package com.gewuyou.forgeboot.i18n.config
import com.gewuyou.forgeboot.core.extension.log
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.context.annotation.Configuration
import org.springframework.web.servlet.config.annotation.InterceptorRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor
/**
*i18n web 配置
*
* @since 2025-02-18 23:33:39
* @author gewuyou
*/
@Configuration
@ConditionalOnProperty(name = ["spring.main.web-application-type"], havingValue = "servlet", matchIfMissing = true)
open class I18nWebConfiguration(
@Autowired
private val localeChangeInterceptor: LocaleChangeInterceptor
) : WebMvcConfigurer {
/**
* Add Spring MVC lifecycle interceptors for pre- and post-processing of
* controller method invocations and resource handler requests.
* Interceptors can be registered to apply to all requests or be limited
* to a subset of URL patterns.
*/
override fun addInterceptors(registry: InterceptorRegistry) {
// Add the locale change interceptor to the registry
log.info("注册语言切换拦截器...")
registry.addInterceptor(localeChangeInterceptor)
}
}

View File

@ -0,0 +1,26 @@
package com.gewuyou.forgeboot.i18n.config.entity
import org.springframework.boot.context.properties.ConfigurationProperties
/**
* i18n属性
*
* @author gewuyou
* @since 2025-02-18 23:59:57
*/
@ConfigurationProperties(prefix = "base-forge.i18n")
class I18nProperties {
/**
* 默认语言
*/
var defaultLocale = "zh_CN"
/**
* 语言请求参数名
*/
var langRequestParameter = "lang"
/**
* 语言文件路径
*/
var wildPathForLanguageFiles = "classpath*:i18n/**/messages"
}

View File

@ -0,0 +1,15 @@
package com.gewuyou.forgeboot.i18n.entity
/**
* 内部信息(用于扩展项目内的i18n信息)
*
* @author gewuyou
* @since 2024-11-26 17:14:15
*/
interface InternalInformation {
/**
* 获取i18n响应信息code
* @return 响应信息 code
*/
val responseI8nMessageCode: String?
}

View File

@ -0,0 +1,21 @@
package com.gewuyou.forgeboot.i18n.entity
/**
* 响应信息(用于对外提供i18n响应信息)
*
* @author gewuyou
* @since 2024-11-26 15:43:06
*/
interface ResponseInformation {
/**
* 获取响应码
* @return 响应码
*/
val responseCode: Int
/**
* 获取i18n响应信息code
* @return 响应信息 code
*/
val responseI8nMessageCode: String
}

View File

@ -0,0 +1,56 @@
package com.gewuyou.forgeboot.i18n.exception;
import com.gewuyou.forgeboot.i18n.entity.ResponseInformation;
/**
* i18n异常
*
* @author gewuyou
* @since 2024-11-12 00:11:32
*/
public class I18nBaseException extends RuntimeException {
/**
* 响应信息对象用于存储错误代码和国际化消息代码
*/
protected final transient ResponseInformation responseInformation;
/**
* 构造函数
*
* @param responseInformation 响应信息对象包含错误代码和国际化消息代码
*/
public I18nBaseException(ResponseInformation responseInformation) {
super();
this.responseInformation = responseInformation;
}
/**
* 构造函数
*
* @param responseInformation 响应信息对象包含错误代码和国际化消息代码
* @param cause 异常原因
*/
public I18nBaseException(ResponseInformation responseInformation, Throwable cause) {
super(cause);
this.responseInformation = responseInformation;
}
/**
* 获取错误代码
*
* @return 错误代码
*/
public int getErrorCode() {
return responseInformation.getResponseCode();
}
/**
* 获取国际化消息代码
*
* @return 国际化消息代码
*/
public String getErrorI18nMessageCode() {
return responseInformation.getResponseI8nMessageCode();
}
}

View File

@ -0,0 +1,51 @@
package com.gewuyou.forgeboot.i18n.filter
import com.gewuyou.forgeboot.i18n.config.entity.I18nProperties
import org.slf4j.LoggerFactory
import org.springframework.context.i18n.LocaleContextHolder
import org.springframework.util.StringUtils
import org.springframework.web.server.ServerWebExchange
import org.springframework.web.server.WebFilterChain
import reactor.core.publisher.Mono
import java.util.*
/**
*反应式 locale 解析器
*
* @since 2025-02-19 00:06:45
* @author gewuyou
*/
class ReactiveLocaleResolver(
private val i18nProperties: I18nProperties
): WebFluxLocaleResolver {
private val log = LoggerFactory.getLogger(ReactiveLocaleResolver::class.java)
/**
* Process the Web request and (optionally) delegate to the next
* `WebFilter` through the given [WebFilterChain].
* @param exchange the current server exchange
* @param chain provides a way to delegate to the next filter
* @return `Mono<Void>` to indicate when request processing is complete
*/
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
val lang = exchange.request.queryParams.getFirst(i18nProperties.langRequestParameter)
if (StringUtils.hasText(lang)) {
log.info("解析到 lang 参数:$lang")
LocaleContextHolder.setLocale(Locale.forLanguageTag(lang)) // 设置语言环境
} else {
// 如果没有 lang 参数,使用 Accept-Language 请求头
val acceptLanguage = exchange.request.headers.getFirst("Accept-Language")
log.info("解析到 Accept-Language 请求头:$acceptLanguage")
if (StringUtils.hasText(acceptLanguage)) {
// 设置语言环境
LocaleContextHolder.setLocale(Locale.forLanguageTag(acceptLanguage))
}else {
// 如果没有 Accept-Language 请求头,使用默认语言环境
LocaleContextHolder.setLocale(Locale.forLanguageTag(i18nProperties.defaultLocale))
}
}
// 继续处理请求
return chain.filter(exchange)
}
}

View File

@ -0,0 +1,5 @@
package com.gewuyou.forgeboot.i18n.filter
import org.springframework.web.server.WebFilter
interface WebFluxLocaleResolver:WebFilter

View File

@ -0,0 +1,2 @@
com.gewuyou.forgeboot.i18n.config.I18nAutoConfiguration
com.gewuyou.forgeboot.i18n.config.I18nWebConfiguration

View File

@ -24,6 +24,7 @@ slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j-version" }
springBootStarter-aop = { group = "org.springframework.boot", name = "spring-boot-starter-aop" }
springBootStarter-web = { group = "org.springframework.boot", name = "spring-boot-starter-web" }
springBootStarter-webflux = { group = "org.springframework.boot", name = "spring-boot-starter-webflux" }
springBootDependencies-bom = { module = "org.springframework.boot:spring-boot-dependencies", version.ref = "spring-boot-version" }
# Libraries can be bundled together for easier import

View File

@ -42,3 +42,10 @@ include(
project(":forgeboot-core").name = "forgeboot-core"
project(":forgeboot-core:forgeboot-core-extension").name = "forgeboot-core-extension"
//endregion
//region i18n
include(
"forgeboot-i18n"
)
project(":forgeboot-i18n").name = "forgeboot-i18n-spring-boot-starter"
//endregion