feat(i18n): Refactored the internationalization module and added Gradle plugin support

- Added I18nKeyGenPlugin Gradle plugin for generating internationalization key files - Refactoring the internationalization module to separate the API and implementation code
- Updated the project structure to create new submodules to support internationalization
- Modify the build configuration to add the necessary dependencies and properties
This commit is contained in:
gewuyou 2025-05-28 21:57:51 +08:00
parent 48228574be
commit b44b5a1570
31 changed files with 324 additions and 116 deletions

View File

@ -13,3 +13,13 @@ dependencies {
// Add a dependency on the Kotlin Gradle plugin, so that convention plugins can apply it. // Add a dependency on the Kotlin Gradle plugin, so that convention plugins can apply it.
implementation(libs.kotlinGradlePlugin) implementation(libs.kotlinGradlePlugin)
} }
gradlePlugin {
plugins {
register("forgeboot-i18n-key-gen") {
id = "i18n-key-gen"
implementationClass = "I18nKeyGenPlugin"
description =
"提供一个用于生成 i18n key文件 的插件"
}
}
}

View File

@ -0,0 +1,184 @@
/**
* 本地化键生成插件
*
* 该插件用于自动生成本地化键的代码以简化国际化(i18n)过程
* 它通过读取属性文件来生成一个包含所有本地化键的Kotlin文件
* 此方法便于集中管理和自动生成代码避免手动编写易出错的字符串字面量
*
* @since 2025-05-28 17:11:40
* @author gewuyou
*/
import org.gradle.api.DefaultTask
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension
import java.util.*
/**
* 可缓存的任务类用于生成本地化键
*
* 该任务读取输入文件中的属性并根据这些属性生成一个Kotlin文件
* 文件中包含根据属性键生成的常量这些键按指定的分组深度和级别进行组织
*/
@CacheableTask
abstract class GenerateI18nKeysTask : DefaultTask() {
/**
* 输入文件属性包含本地化属性
* 被注解为@InputFile和@PathSensitive以确保Gradle可以正确地检测文件路径的变化
*/
@get:InputFile
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val inputFile: RegularFileProperty
/**
* 输出文件属性将生成的代码写入此文件
* 被注解为@OutputFile以告知Gradle该文件是任务的输出
*/
@get:OutputFile
abstract val outputFile: RegularFileProperty
/**
* 根包名属性用于指定生成代码的包路径
* 被注解为@Input因为包路径的变化应导致任务重新执行
*/
@get:Input
abstract val rootPackage: Property<String>
/**
* 层级属性决定键的分组深度
* 注解为@Input因为层级变化会影响生成的代码结构
*/
@get:Input
abstract val level: Property<Int>
/**
* 任务的主要执行方法
*
* 该方法读取输入文件中的属性然后根据这些属性生成一个Kotlin文件
* 它首先加载属性然后根据指定的包路径层级和分组深度生成代码
*/
@TaskAction
fun generate() {
// 加载属性文件
val props = Properties().apply {
inputFile.get().asFile.inputStream().use { load(it) }
}
// 准备输出文件
val output = outputFile.get().asFile
output.parentFile.mkdirs()
// 处理属性键,按分组深度分组
val keys = props.stringPropertyNames().sorted()
val levelDepth = level.get()
val indentUnit = " "
fun Appendable.generateObjects(keys: List<String>, depth: Int) {
val groups = keys.groupBy { it.split(".").take(depth).joinToString(".") }
groups.forEach { (groupKey, groupKeys) ->
val segments = groupKey.split(".")
val indent = indentUnit.repeat(segments.size)
val objectName = segments.last().replaceFirstChar(Char::uppercase)
appendLine("$indent object $objectName {")
val deeperKeys = groupKeys.filter { it.split(".").size > depth }
if (depth < levelDepth && deeperKeys.isNotEmpty()) {
generateObjects(deeperKeys, depth + 1)
} else {
groupKeys.forEach { fullKey ->
val constName = fullKey
.split(".")
.drop(levelDepth)
.joinToString("_") { it.uppercase() }
.ifBlank { fullKey.replace(".", "_").uppercase() }
appendLine("${indent}$indentUnit const val $constName = \"$fullKey\"")
}
}
appendLine("$indent }")
}
}
// 生成代码并写入输出文件
output.writeText(buildString {
appendLine("package ${rootPackage.get()}")
appendLine("// ⚠️ Auto-generated. Do not edit manually.")
appendLine("object I18nKeys {")
generateObjects(keys, 1)
appendLine("}")
})
println("✅ Generated I18nKeys.kt at ${output.absolutePath}")
}
}
/**
* 扩展类用于配置插件
*
* 该扩展定义了插件的可配置属性包括层级分组深度和根包名
*/
interface I18nKeyGenExtension {
val level: Property<Int>
val rootPackage: Property<String>
val inputPath: Property<String>
val fileName: Property<String>
val readPath: Property<String>
val readFileName: Property<String>
}
/**
* 本地化键生成插件的实现
*
* 该插件在项目中添加扩展和任务以生成本地化键的代码
*/
// ---- 插件主体 ----
class I18nKeyGenPlugin : Plugin<Project> {
override fun apply(project: Project) {
val ext = project.extensions.create("i18nKeyGen", I18nKeyGenExtension::class.java).apply {
level.convention(2)
rootPackage.convention("com.gewuyou.i18n")
inputPath.convention("generated/i18n") // 子路径
fileName.convention("I18nKeys.kt")
readPath.convention("src/main/resources/i18n")
readFileName.convention("messages.properties")
}
project.afterEvaluate {
val propsFile = project.file("${ext.readPath.get()}/${ext.readFileName.get()}")
if (!propsFile.exists()) return@afterEvaluate
// 输出文件路径
val outputFile = project.layout.buildDirectory.file("${ext.inputPath.get()}/${ext.fileName.get()}")
val outputDir = project.layout.buildDirectory.dir(ext.inputPath)
// 注册生成任务
val generateTask = project.tasks.register("generateI18nKeys", GenerateI18nKeysTask::class.java) {
setProperty("inputFile", propsFile)
setProperty("outputFile", outputFile)
setProperty("level", ext.level)
setProperty("rootPackage", ext.rootPackage)
}
// 👇 确保编译前依赖该任务
listOf("compileKotlin", "kaptGenerateStubsKotlin").forEach { name ->
project.tasks.matching { it.name == name }.configureEach {
dependsOn(generateTask)
}
}
// 👇 延迟注册源码目录:使用 map 避免提前访问目录
project.plugins.withId("org.jetbrains.kotlin.jvm") {
val kotlinExt = project.extensions.getByType(KotlinProjectExtension::class.java)
kotlinExt.sourceSets.getByName("main").kotlin.srcDir(outputDir.map { it.asFile })
}
}
}
}

View File

@ -22,4 +22,16 @@ object Modules {
const val RESULT_IMPL = ":forgeboot-common:forgeboot-common-result:forgeboot-common-result-impl" const val RESULT_IMPL = ":forgeboot-common:forgeboot-common-result:forgeboot-common-result-impl"
const val RESULT_API = ":forgeboot-common:forgeboot-common-result:forgeboot-common-result-api" const val RESULT_API = ":forgeboot-common:forgeboot-common-result:forgeboot-common-result-api"
} }
object I18N {
const val STARTER = ":forgeboot-i18n-spring-boot-starter"
const val API = ":forgeboot-i18n-spring-boot-starter:forgeboot-i18n-api"
const val IMPL = ":forgeboot-i18n-spring-boot-starter:forgeboot-i18n-impl"
const val AUTOCONFIGURE = ":forgeboot-i18n-spring-boot-starter:forgeboot-i18n-autoconfigure"
}
object TRACE{
const val STARTER = ":forgeboot-trace-spring-boot-starter"
const val API = ":forgeboot-trace-spring-boot-starter:forgeboot-trace-api"
const val IMPL = ":forgeboot-trace-spring-boot-starter:forgeboot-trace-impl"
const val AUTOCONFIGURE = ":forgeboot-trace-spring-boot-starter:forgeboot-trace-autoconfigure"
}
} }

View File

@ -1,3 +1,5 @@
object ProjectFlags { object ProjectFlags {
const val IS_ROOT_MODULE = "isRootModule" const val IS_ROOT_MODULE = "isRootModule"
const val IS_PUBLISH_MODULE = "isPublishModule"
const val ARTIFACT_ID = "artifactId"
} }

View File

@ -1,8 +1,6 @@
dependencies { extra{
implementation(project(Modules.Core.EXTENSION)) setProperty(ProjectFlags.IS_ROOT_MODULE,true)
implementation(project(Modules.Common.RESULT_API)) }
dependencies {
compileOnly(libs.springBootStarter.web)
// Spring Boot WebFlux
compileOnly(libs.springBootStarter.webflux)
} }

View File

@ -0,0 +1,8 @@
dependencies {
compileOnly(platform(libs.springBootDependencies.bom))
// compileOnly(platform(libs.springCloudDependencies.bom))
// compileOnly(libs.springBootStarter.web)
compileOnly(libs.springBootStarter.webflux)
}

View File

@ -1,4 +1,4 @@
package com.gewuyou.forgeboot.i18n.entity package com.gewuyou.forgeboot.i18n.api
/** /**
* 内部信息(用于扩展项目内的i18n信息) * 内部信息(用于扩展项目内的i18n信息)
@ -12,4 +12,10 @@ interface InternalInformation {
* @return 响应信息 code * @return 响应信息 code
*/ */
val responseI8nMessageCode: String? val responseI8nMessageCode: String?
/**
* 获取i18n响应信息参数
* @return 响应信息 参数数组
*/
val responseI8nMessageArgs: Array<Any>?
} }

View File

@ -1,4 +1,4 @@
package com.gewuyou.forgeboot.common.result.api package com.gewuyou.forgeboot.i18n.api
/** /**
* 消息解析器接口 * 消息解析器接口
* *

View File

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

View File

@ -0,0 +1 @@
spring.application.name=forgeboot-i18n-api

View File

@ -0,0 +1,10 @@
dependencies {
compileOnly(platform(libs.springBootDependencies.bom))
compileOnly(platform(libs.springCloudDependencies.bom))
compileOnly(libs.springBootStarter.web)
compileOnly(libs.springBootStarter.webflux)
compileOnly(project(Modules.I18N.API))
compileOnly(project(Modules.I18N.IMPL))
implementation(project(Modules.Core.EXTENSION))
}

View File

@ -1,10 +1,11 @@
package com.gewuyou.forgeboot.i18n.config package com.gewuyou.forgeboot.i18n.autoconfigure
import com.gewuyou.forgeboot.common.result.api.MessageResolver
import com.gewuyou.forgeboot.i18n.filter.ReactiveLocaleResolver
import com.gewuyou.forgeboot.core.extension.log import com.gewuyou.forgeboot.core.extension.log
import com.gewuyou.forgeboot.i18n.config.entity.I18nProperties import com.gewuyou.forgeboot.i18n.api.MessageResolver
import com.gewuyou.forgeboot.i18n.resolver.I18nMessageResolver import com.gewuyou.forgeboot.i18n.impl.config.I18nProperties
import com.gewuyou.forgeboot.i18n.impl.filter.ReactiveLocaleResolver
import com.gewuyou.forgeboot.i18n.impl.resolver.I18nMessageResolver
import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletRequest
import org.springframework.beans.factory.annotation.Qualifier import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass import org.springframework.boot.autoconfigure.condition.ConditionalOnClass

View File

@ -1,4 +1,4 @@
package com.gewuyou.forgeboot.i18n.config package com.gewuyou.forgeboot.i18n.autoconfigure
import com.gewuyou.forgeboot.core.extension.log import com.gewuyou.forgeboot.core.extension.log
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired

View File

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

View File

@ -0,0 +1,7 @@
dependencies {
compileOnly(platform(libs.springBootDependencies.bom))
compileOnly(platform(libs.springCloudDependencies.bom))
compileOnly(libs.springBootStarter.web)
compileOnly(libs.springBootStarter.webflux)
compileOnly(project(Modules.I18N.API))
}

View File

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

View File

@ -1,14 +1,17 @@
package com.gewuyou.forgeboot.i18n.exception package com.gewuyou.forgeboot.i18n.impl.exception
import com.gewuyou.forgeboot.i18n.entity.ResponseInformation
/** /**
* i18n异常 * i18n异常
* *
* 该异常类用于处理国际化相关的异常情况提供了错误代码和国际化消息代码的封装
* 以便于在异常处理时能够方便地获取这些信息
*
* @author gewuyou * @author gewuyou
* @since 2024-11-12 00:11:32 * @since 2024-11-12 00:11:32
*/ */
class I18nBaseException : RuntimeException { open class I18nBaseException : RuntimeException {
/** /**
* 响应信息对象用于存储错误代码和国际化消息代码 * 响应信息对象用于存储错误代码和国际化消息代码
*/ */
@ -18,19 +21,25 @@ class I18nBaseException : RuntimeException {
/** /**
* 构造函数 * 构造函数
* *
* 初始化异常对象用于当只有响应信息可用时
*
* @param responseInformation 响应信息对象包含错误代码和国际化消息代码 * @param responseInformation 响应信息对象包含错误代码和国际化消息代码
*/ */
constructor(responseInformation: ResponseInformation) : super() { constructor(responseInformation: ResponseInformation)
: super(responseInformation.responseI8nMessageCode) {
this.responseInformation = responseInformation this.responseInformation = responseInformation
} }
/** /**
* 构造函数 * 构造函数
* *
* 初始化异常对象用于当有响应信息和异常原因时
*
* @param responseInformation 响应信息对象包含错误代码和国际化消息代码 * @param responseInformation 响应信息对象包含错误代码和国际化消息代码
* @param cause 异常原因 * @param cause 异常原因
*/ */
constructor(responseInformation: ResponseInformation, cause: Throwable?) : super(cause) { constructor(responseInformation: ResponseInformation, cause: Throwable)
: super(responseInformation.responseI8nMessageCode, cause) {
this.responseInformation = responseInformation this.responseInformation = responseInformation
} }
@ -49,4 +58,12 @@ class I18nBaseException : RuntimeException {
*/ */
val errorI18nMessageCode: String val errorI18nMessageCode: String
get() = responseInformation.responseI8nMessageCode get() = responseInformation.responseI8nMessageCode
/**
* 获取国际化消息参数
*
* @return 国际化消息参数数组
*/
val errorI18nMessageArgs: Array<Any>
get() = responseInformation.args
} }

View File

@ -1,7 +1,9 @@
package com.gewuyou.forgeboot.i18n.filter package com.gewuyou.forgeboot.i18n.impl.filter
import com.gewuyou.forgeboot.i18n.config.entity.I18nProperties import com.gewuyou.forgeboot.i18n.api.WebFluxLocaleResolver
import com.gewuyou.forgeboot.i18n.impl.config.I18nProperties
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.context.i18n.LocaleContextHolder import org.springframework.context.i18n.LocaleContextHolder
import org.springframework.util.StringUtils import org.springframework.util.StringUtils

View File

@ -1,6 +1,7 @@
package com.gewuyou.forgeboot.i18n.resolver package com.gewuyou.forgeboot.i18n.impl.resolver
import com.gewuyou.forgeboot.common.result.api.MessageResolver
import com.gewuyou.forgeboot.i18n.api.MessageResolver
import org.springframework.context.MessageSource import org.springframework.context.MessageSource
import org.springframework.context.i18n.LocaleContextHolder import org.springframework.context.i18n.LocaleContextHolder

View File

@ -0,0 +1 @@
spring.application.name=forgeboot-i18n-impl-starter

View File

@ -1,63 +0,0 @@
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 = "forgeboot.i18n")
public class I18nProperties {
/**
* 默认语言
*/
private String defaultLocale = "zh_CN";
/**
* 语言请求参数名
*/
private String langRequestParameter = "lang";
/**
* 语言文件路径
*/
private String wildPathForLanguageFiles = "classpath*:i18n/**/messages";
/**
* 位置模式后缀
*/
private String locationPatternSuffix = ".properties";
public String getLocationPatternSuffix() {
return locationPatternSuffix;
}
public void setLocationPatternSuffix(String locationPatternSuffix) {
this.locationPatternSuffix = locationPatternSuffix;
}
public String getDefaultLocale() {
return defaultLocale;
}
public void setDefaultLocale(String defaultLocale) {
this.defaultLocale = defaultLocale;
}
public String getLangRequestParameter() {
return langRequestParameter;
}
public void setLangRequestParameter(String langRequestParameter) {
this.langRequestParameter = langRequestParameter;
}
public String getWildPathForLanguageFiles() {
return wildPathForLanguageFiles;
}
public void setWildPathForLanguageFiles(String wildPathForLanguageFiles) {
this.wildPathForLanguageFiles = wildPathForLanguageFiles;
}
}

View File

@ -1,21 +0,0 @@
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

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

View File

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