mirror of
https://github.moeyy.xyz/https://github.com/GeWuYou/forgeboot
synced 2025-10-27 23:58:53 +08:00
Compare commits
3 Commits
fc8bea06b3
...
42eadcece0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42eadcece0 | ||
|
|
59ee9e4d1b | ||
|
|
e349dc785d |
3
forgeboot-i18n/.gitattributes
vendored
Normal file
3
forgeboot-i18n/.gitattributes
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/gradlew text eol=lf
|
||||||
|
*.bat text eol=crlf
|
||||||
|
*.jar binary
|
||||||
40
forgeboot-i18n/.gitignore
vendored
Normal file
40
forgeboot-i18n/.gitignore
vendored
Normal 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
|
||||||
11
forgeboot-i18n/build.gradle.kts
Normal file
11
forgeboot-i18n/build.gradle.kts
Normal 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)
|
||||||
|
}
|
||||||
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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"
|
||||||
|
}
|
||||||
@ -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?
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
package com.gewuyou.forgeboot.i18n.exception
|
||||||
|
|
||||||
|
import com.gewuyou.forgeboot.i18n.entity.ResponseInformation
|
||||||
|
|
||||||
|
/**
|
||||||
|
* i18n异常
|
||||||
|
*
|
||||||
|
* @author gewuyou
|
||||||
|
* @since 2024-11-12 00:11:32
|
||||||
|
*/
|
||||||
|
class I18nBaseException : RuntimeException {
|
||||||
|
/**
|
||||||
|
* 响应信息对象,用于存储错误代码和国际化消息代码
|
||||||
|
*/
|
||||||
|
@Transient
|
||||||
|
private val responseInformation: ResponseInformation
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*
|
||||||
|
* @param responseInformation 响应信息对象,包含错误代码和国际化消息代码
|
||||||
|
*/
|
||||||
|
constructor(responseInformation: ResponseInformation) : super() {
|
||||||
|
this.responseInformation = responseInformation
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*
|
||||||
|
* @param responseInformation 响应信息对象,包含错误代码和国际化消息代码
|
||||||
|
* @param cause 异常原因
|
||||||
|
*/
|
||||||
|
constructor(responseInformation: ResponseInformation, cause: Throwable?) : super(cause) {
|
||||||
|
this.responseInformation = responseInformation
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取错误代码
|
||||||
|
*
|
||||||
|
* @return 错误代码
|
||||||
|
*/
|
||||||
|
val errorCode: Int
|
||||||
|
get() = responseInformation.responseCode
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取国际化消息代码
|
||||||
|
*
|
||||||
|
* @return 国际化消息代码
|
||||||
|
*/
|
||||||
|
val errorI18nMessageCode: String
|
||||||
|
get() = responseInformation.responseI8nMessageCode
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
package com.gewuyou.forgeboot.i18n.filter
|
||||||
|
|
||||||
|
import org.springframework.web.server.WebFilter
|
||||||
|
|
||||||
|
interface WebFluxLocaleResolver:WebFilter
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
com.gewuyou.forgeboot.i18n.config.I18nAutoConfiguration
|
||||||
|
com.gewuyou.forgeboot.i18n.config.I18nWebConfiguration
|
||||||
@ -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-aop = { group = "org.springframework.boot", name = "spring-boot-starter-aop" }
|
||||||
springBootStarter-web = { group = "org.springframework.boot", name = "spring-boot-starter-web" }
|
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" }
|
springBootDependencies-bom = { module = "org.springframework.boot:spring-boot-dependencies", version.ref = "spring-boot-version" }
|
||||||
|
|
||||||
# Libraries can be bundled together for easier import
|
# Libraries can be bundled together for easier import
|
||||||
|
|||||||
@ -41,4 +41,11 @@ include(
|
|||||||
)
|
)
|
||||||
project(":forgeboot-core").name = "forgeboot-core"
|
project(":forgeboot-core").name = "forgeboot-core"
|
||||||
project(":forgeboot-core:forgeboot-core-extension").name = "forgeboot-core-extension"
|
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
|
//endregion
|
||||||
Loading…
x
Reference in New Issue
Block a user