From 2c9a2d332abed97bbf1aa41362ee0536915d573b Mon Sep 17 00:00:00 2001 From: Gareth Coles Date: Sat, 4 Jan 2025 16:49:32 +0000 Subject: [PATCH] 1.1.0-SNAPSHOT: Let's get serious about this. --- .github/workflows/tag.yml | 3 + build.gradle.kts | 10 - buildSrc/build.gradle.kts | 1 + buildSrc/src/main/kotlin/general.gradle.kts | 8 - buildSrc/src/main/kotlin/published.gradle.kts | 64 ++++-- gradle.properties | 2 +- gradle/libs.versions.toml | 10 + i18n-generator/build.gradle.kts | 4 + .../kotlin/dev/kordex/i18n/generator/Main.kt | 191 +++++++++++++----- .../i18n/generator/TranslationsClass.kt | 135 +++++++------ .../dev/kordex/i18n/generator/_Formatting.kt | 42 ++++ .../main/resources/simplelogger.properties | 10 + .../src/main/kotlin/dev/kordex/i18n/Bundle.kt | 6 +- .../dev/kordex/i18n/files/FileFormat.kt | 14 ++ .../kordex/i18n/files/PropertiesControl.kt | 33 --- .../dev/kordex/i18n/files/PropertiesFormat.kt | 39 ++++ .../dev/kordex/i18n/files/YamlFormat.kt | 17 ++ .../i18n/registries/FileFormatRegistry.kt | 45 +++-- .../i18n/registries/MessageFormatRegistry.kt | 3 + .../kotlin/tests/files/PropertiesTests.kt | 16 +- .../test/kotlin/tests/files/RegistryTests.kt | 36 ++-- test/strings.yml | 20 ++ 22 files changed, 480 insertions(+), 229 deletions(-) create mode 100644 i18n-generator/src/main/kotlin/dev/kordex/i18n/generator/_Formatting.kt create mode 100644 i18n-generator/src/main/resources/simplelogger.properties create mode 100644 i18n/src/main/kotlin/dev/kordex/i18n/files/FileFormat.kt delete mode 100644 i18n/src/main/kotlin/dev/kordex/i18n/files/PropertiesControl.kt create mode 100644 i18n/src/main/kotlin/dev/kordex/i18n/files/PropertiesFormat.kt create mode 100644 i18n/src/main/kotlin/dev/kordex/i18n/files/YamlFormat.kt create mode 100644 test/strings.yml diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml index 51e4c97..7f1d3d7 100644 --- a/.github/workflows/tag.yml +++ b/.github/workflows/tag.yml @@ -53,6 +53,9 @@ jobs: env: ORG_GRADLE_PROJECT_signingKey: "${{ secrets.GPG_KEY }}" ORG_GRADLE_PROJECT_signingPassword: "${{ secrets.GPG_PASSWORD }}" + ORG_GRADLE_PROJECT_mavenCentralUsername: "${{ secrets.MAVEN_CENTRAL_USERNAME }}" + ORG_GRADLE_PROJECT_mavenCentralPassword: "${{ secrets.MAVEN_CENTRAL_PASSWORD }}" + ORG_GRADLE_PROJECT_publishingTag: "true" KORDEX_MAVEN_PASSWORD: "${{ secrets.KORDEX_MAVEN_PASSWORD }}" KORDEX_MAVEN_USERNAME: "${{ secrets.KORDEX_MAVEN_USERNAME }}" diff --git a/build.gradle.kts b/build.gradle.kts index 56e77ec..38a06b1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,17 +1,7 @@ -import org.jetbrains.dokka.DokkaConfiguration -import org.jetbrains.dokka.gradle.DokkaTaskPartial -import org.jetbrains.dokka.gradle.formats.DokkaJavadocPlugin - plugins { id("org.jetbrains.dokka") version "2.0.0" } subprojects { apply(plugin = "org.jetbrains.dokka") - - task("javadocJar", Jar::class) { - dependsOn(tasks.dokkaGeneratePublicationHtml) - from(tasks.dokkaGeneratePublicationHtml.flatMap { it.outputDirectory }) - archiveClassifier = "javadoc" - } } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 987ca2d..96b2538 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { implementation("dev.yumi", "yumi-gradle-licenser", "1.2.0") implementation("io.gitlab.arturbosch.detekt", "detekt-gradle-plugin", "1.23.6") + implementation("com.vanniktech.maven.publish.base", "com.vanniktech.maven.publish.base.gradle.plugin", "0.30.0") } beforeEvaluate { diff --git a/buildSrc/src/main/kotlin/general.gradle.kts b/buildSrc/src/main/kotlin/general.gradle.kts index 71bdc09..8c681f2 100644 --- a/buildSrc/src/main/kotlin/general.gradle.kts +++ b/buildSrc/src/main/kotlin/general.gradle.kts @@ -1,5 +1,3 @@ -import org.gradle.kotlin.dsl.from - plugins { id("dev.yumi.gradle.licenser") id("io.gitlab.arturbosch.detekt") @@ -31,9 +29,3 @@ license { kotlin { explicitApi() } - -val sourceJar = task("sourceJar", Jar::class) { - dependsOn(tasks["classes"]) - archiveClassifier = "sources" - from(sourceSets.main.get().allSource) -} diff --git a/buildSrc/src/main/kotlin/published.gradle.kts b/buildSrc/src/main/kotlin/published.gradle.kts index df2e93a..a334fcc 100644 --- a/buildSrc/src/main/kotlin/published.gradle.kts +++ b/buildSrc/src/main/kotlin/published.gradle.kts @@ -1,46 +1,55 @@ -import org.gradle.kotlin.dsl.from -import kotlin.text.get -import kotlin.text.set +import com.vanniktech.maven.publish.JavadocJar +import com.vanniktech.maven.publish.KotlinJvm +import com.vanniktech.maven.publish.SonatypeHost plugins { `maven-publish` signing + + id("com.vanniktech.maven.publish.base") } -val sourceJar: Task by tasks.getting -val javadocJar: Task by tasks.getting -//val dokkaJar: Task by tasks.getting +val isSnapshot = project.version.toString().contains("SNAPSHOT") +val isTag = (project.findProperty("publishingTag") as String?) == "true" afterEvaluate { publishing { repositories { maven { - name = "KordEx" + name = "kordEx" - url = if (project.version.toString().contains("SNAPSHOT")) { + url = if (isSnapshot) { uri("https://repo.kordex.dev/snapshots/") } else { uri("https://repo.kordex.dev/releases/") } credentials { - username = project.findProperty("ossrhUsername") as String? + username = project.findProperty("kordexMavenUsername") as String? ?: System.getenv("KORDEX_MAVEN_USERNAME") - password = project.findProperty("ossrhPassword") as String? + password = project.findProperty("kordexMavenPassword") as String? ?: System.getenv("KORDEX_MAVEN_PASSWORD") } version = project.version } - } - publications { - create("maven") { - from(components.getByName("java")) + mavenPublishing { + if (isTag && !isSnapshot) { + publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL, true) + } + + configure( + KotlinJvm( + javadocJar = JavadocJar.Dokka("dokkaGeneratePublicationHtml"), + sourcesJar = true + ) + ) + + signAllPublications() - artifact(sourceJar) -// artifact(javadocJar) + coordinates(project.group.toString(), project.name, project.version.toString()) pom { name.set(project.ext.get("pubName").toString()) @@ -83,3 +92,26 @@ afterEvaluate { sign(publishing.publications["maven"]) } } + +afterEvaluate { + project.publishing.publications.forEach { publication -> + if (publication is MavenPublication) { + println(">> Publication: ${publication.groupId}:${publication.artifactId}:${publication.version}") + + println( + " Classifiers: " + + publication.artifacts + .filter { artifact -> artifact.classifier != null } + .sortedBy { it.classifier } + .joinToString { it -> "${it.classifier}:${it.extension}" } + ) + + println( + " Repos: " + + project.publishing.repositories + .sortedBy { it.name } + .joinToString { it.name } + ) + } + } +} diff --git a/gradle.properties b/gradle.properties index b4bd996..c3a6d48 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,4 +6,4 @@ org.gradle.parallel=true org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true -projectVersion=1.0.7 +projectVersion=1.1.0-SNAPSHOT diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e9d54fc..9441c56 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,8 @@ [versions] +kotlin = "2.0.21" + icu4j = "76.1" +ktlint = "1.5.0" kx-ser = "1.7.3" logging = "7.0.0" slf4j = "2.0.16" @@ -12,6 +15,12 @@ yaml-resource-bundle = "2.13.0" icu4j = { module = "com.ibm.icu:icu4j", version.ref = "icu4j" } kx-ser = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kx-ser" } kotlin-logging = { module = "io.github.oshai:kotlin-logging", version.ref = "logging" } +kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } + +ktlint-engine = { module = "com.pinterest.ktlint:ktlint-rule-engine", version.ref = "ktlint" } +ktlint-cli-rules = { module = "com.pinterest.ktlint:ktlint-cli-ruleset-core", version.ref = "ktlint" } +ktlint-rules = { module = "com.pinterest.ktlint:ktlint-ruleset-standard", version.ref = "ktlint" } + slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" } @@ -22,6 +31,7 @@ kotest-property = { module = "io.kotest:kotest-property", version.ref = "kotest" yaml-resource-bundle = { module = "dev.akkinoc.util:yaml-resource-bundle", version.ref = "yaml-resource-bundle" } [bundles] +ktlint = ["ktlint-engine", "ktlint-cli-rules", "ktlint-rules"] logging = ["kotlin-logging", "slf4j"] resources = ["yaml-resource-bundle"] testing = ["kotest-assertions", "kotest-framework", "kotest-property"] diff --git a/i18n-generator/build.gradle.kts b/i18n-generator/build.gradle.kts index 92833a2..67d1fd3 100644 --- a/i18n-generator/build.gradle.kts +++ b/i18n-generator/build.gradle.kts @@ -36,9 +36,13 @@ repositories { dependencies { implementation("info.picocli:picocli:4.7.6") + implementation(libs.kotlin.reflect) + implementation(libs.slf4j.simple) api("com.hanggrian:kotlinpoet-dsl:0.2") api("com.squareup:kotlinpoet:1.18.1") + api(libs.bundles.ktlint) + api(project(":i18n")) } val propsTask = tasks.register("kordExProps") { diff --git a/i18n-generator/src/main/kotlin/dev/kordex/i18n/generator/Main.kt b/i18n-generator/src/main/kotlin/dev/kordex/i18n/generator/Main.kt index d697ea1..bdf06ce 100644 --- a/i18n-generator/src/main/kotlin/dev/kordex/i18n/generator/Main.kt +++ b/i18n-generator/src/main/kotlin/dev/kordex/i18n/generator/Main.kt @@ -6,15 +6,87 @@ package dev.kordex.i18n.generator +import dev.kordex.i18n.files.FileFormat +import dev.kordex.i18n.files.PropertiesFormat +import dev.kordex.i18n.messages.MessageFormat +import dev.kordex.i18n.messages.formats.ICUFormatV1 +import dev.kordex.i18n.registries.FileFormatRegistry +import dev.kordex.i18n.registries.MessageFormatRegistry import picocli.CommandLine import picocli.CommandLine.Model.OptionSpec import java.io.File -import java.nio.charset.Charset -import java.nio.file.Files +import java.net.URLClassLoader +import java.nio.file.Path import java.util.* +import kotlin.reflect.KClass +import kotlin.reflect.full.createInstance import kotlin.system.exitProcess +public val extraFileFormats: List = ( + System.getProperties()["fileFormats"] + ?: System.getenv()["FILE_FORMATS"] + ) + ?.toString() + ?.split(',') + ?: emptyList() + +public val extraMessageFormats: List = ( + System.getProperties()["messageFormats"] + ?: System.getenv()["MESSAGE_FORMATS"] + ) + ?.toString() + ?.split(',') + ?: emptyList() + public fun main(vararg args: String) { + if (extraFileFormats.isNotEmpty()) { + println("Loading ${extraFileFormats.size} extra file format/s...") + + extraFileFormats.forEach { specifier -> + try { + @Suppress("UNCHECKED_CAST") + val clazz = Class.forName(specifier).kotlin as KClass + + val obj = clazz.objectInstance + ?: clazz.createInstance() + + FileFormatRegistry.register(obj) + + println("\t$specifier -> Loaded successfully") + } catch (_: ClassNotFoundException) { + println("\t$specifier -> Failed: Type not found, perhaps try appending 'Kt'") + } catch (_: ClassCastException) { + println("\t$specifier -> Failed: Type doesn't extend FileFormat") + } + } + + println() + } + + if (extraMessageFormats.isNotEmpty()) { + println("Loading ${extraMessageFormats.size} extra message format/s...") + + extraMessageFormats.forEach { specifier -> + try { + @Suppress("UNCHECKED_CAST") + val clazz = Class.forName(specifier).kotlin as KClass + + val obj = clazz.objectInstance + ?: clazz.createInstance() + + MessageFormatRegistry.register(obj) + + println("\t$specifier -> Loaded successfully") + } catch (_: ClassNotFoundException) { + println("\t$specifier -> Failed: Type not found, perhaps try appending 'Kt'") + } catch (_: ClassCastException) { + println("\t$specifier -> Failed: Type doesn't extend FileFormat") + } + } + + println() + } + val spec = CommandLine.Model.CommandSpec.create() spec.name("i18n-generator") @@ -26,10 +98,23 @@ public fun main(vararg args: String) { spec.usageMessage() .description( - "%nCommand-line tool for generating Kord Extensions translations classes from translation bundle " + - "properties files.%n" + "%nCommand-line tool for generating translations classes from translation bundle files.%n", + + "Use '-DfileFormats' or the 'FILE_FORMATS' environmental variable to specify third-party file formats, " + + "represented by a comma-delimited list of fully-qualified names.%n", + "Use '-DmessageFormats' or the 'MESSAGE_FORMATS' environmental variable to specify third-party message " + + "formats, represented by a comma-delimited list of fully-qualified names.%n", + + "All specified file formats must implement the FileFormat type, and message formats must implement the " + + "MessageFormat type. For more information, please see the documentation.%n", + + "Lists of available file formats and message formats can be found at the bottom of this " + + "help message. %n" + ) + .footer( + "%nAvailable file formats: " + FileFormatRegistry.getFormats().sorted().joinToString(), + "Available message formats: " + MessageFormatRegistry.getFormats().sorted().joinToString() ) - .footer("%nAvailable encodings: " + Charset.availableCharsets().keys.joinToString()) spec.version(null) spec.mixinStandardHelpOptions(true) @@ -41,11 +126,19 @@ public fun main(vararg args: String) { description("Name of the relevant translations bundle, which will be included in the output.") } - spec.addOption("-i", "--input-file") { + spec.addOption("-i", "--input-path") { paramLabel("INPUT") required(true) - description("Input file, a properties file representing a set of translations from a translation bundle.") + description( + "Input path, pointing to the directory containing your translation bundle, " + + "relative to the bundle you specified. For example, if your bundle is 'kordex.strings' and you're " + + "using the 'properties' file format, you should provide the path to a directory containing " + + "'kordex/strings.properties'.", + "", + "In most situations, this should be the 'translations' directory in your project's " + + "'src/main/resources' directory." + ) } spec.addOption("-p", "--class-package") { @@ -62,20 +155,29 @@ public fun main(vararg args: String) { description("Generated class name. Defaults to \"Translations\".") } - spec.addOption("-mfv", "--message-format-version") { - paramLabel("VERSION") - defaultValue("1") + spec.addOption("-f", "--file-format") { + paramLabel("FILE-FORMAT") + defaultValue("properties") - description("ICU Message Format version. Defaults to version 1, but you may specify version 2 if needed.") + description( + "Translations file format identifier. Defaults to '${PropertiesFormat.identifiers.first()}'." + ) } - spec.addOption("-e", "--encoding") { - paramLabel("ENCODING") - defaultValue("UTF-8") + spec.addOption("-m", "--message-format") { + paramLabel("MESSAGE-FORMAT") + defaultValue(ICUFormatV1.identifier) - Charset.availableCharsets().keys + description( + "Message format identifier. Defaults to ${ICUFormatV1.identifier}." + ) + } + + spec.addOption("--editorconfig") { + paramLabel("PATH") + defaultValue(".editorconfig") - description("Character encoding used to load the bundle file. Defaults to UTF-8.") + description("Path to a .editorconfig file to use when formatting generated code. Defaults to '.editorconfig'.") } spec.addOption("-o", "--output-dir") { @@ -97,16 +199,6 @@ public fun main(vararg args: String) { ) } - spec.addOption("-ncc", "--no-camel-case") { - paramLabel("CAMEL CASE") - defaultValue("false") - - description( - "Replace common delimiters in names with underscores instead of camel-casing them. " + - "This option is provided for compatibility, and will be removed in the future.." - ) - } - val commandLine = CommandLine(spec) commandLine.setExecutionStrategy(::run) @@ -122,29 +214,18 @@ private fun run(result: CommandLine.ParseResult): Int { } var bundle: String = result.matchedOption("b").getValue() - val inputFile: File = result.matchedOption("i").getValue() + val inputPath: File = result.matchedOption("i").getValue() val classPackage: String = result.matchedOption("p").getValue() - val encoding: String = result.matchedOptionValue("e", "UTF-8") val internal: Boolean = result.matchedOptionValue("in", false) - val noCamelCase: Boolean = result.matchedOptionValue("ncc", false) - val messageFormatVersion: Int = result.matchedOptionValue("mfv", 1) + val fileFormat: String = result.matchedOptionValue("f", PropertiesFormat.identifiers.first()) + val messageFormat: String = result.matchedOptionValue("m", ICUFormatV1.identifier) + val editorConfig: File = result.matchedOptionValue("editorconfig", File(".editorconfig")) val className: String = result.matchedOptionValue("c", "Translations") val outputDir: File = result.matchedOptionValue("o", File("output")) - if (messageFormatVersion !in MESSAGE_FORMAT_VERSIONS) { - exitError( - "Invalid message format version $messageFormatVersion - " + - "must be one of ${MESSAGE_FORMAT_VERSIONS.joinToString()}" - ) - } - - if ("." !in bundle) { - bundle = "$bundle.strings" - } - - if (!inputFile.exists()) { - exitError("Unable to find ${inputFile.absolutePath}") + if (!inputPath.exists()) { + exitError("Unable to find ${inputPath.absolutePath}") } if (!outputDir.exists()) { @@ -152,28 +233,30 @@ private fun run(result: CommandLine.ParseResult): Int { outputDir.mkdirs() } - println("Loading properties...") + println("Loading translations...") - val props = Properties() + val fileFormatObj = FileFormatRegistry.getOrError(fileFormat) + val loader = URLClassLoader(arrayOf(inputPath.toURI().toURL())) - props.load( - Files.newBufferedReader( - inputFile.toPath(), - charset(encoding) - ) + val resourceBundle = ResourceBundle.getBundle( + bundle.replace(".", "/"), + Locale.of("dummy"), + loader, + fileFormatObj.control, ) - println("Found ${props.size} translation keys.") + println("Found ${resourceBundle.keys.toList().size} translation keys.") println("Generating class \"$className\" for bundle \"$bundle\"...") val translationsClass = TranslationsClass( - allProps = props, + resourceBundle = resourceBundle, bundle = bundle, className = className, publicVisibility = !internal, - splitToCamelCase = !noCamelCase, classPackage = classPackage, - messageFormatVersion = messageFormatVersion, + fileFormat = FileFormatRegistry.getOrError(fileFormat), + messageFormat = messageFormat, + editorConfig = Path.of(editorConfig.toURI()) ) translationsClass.writeTo(outputDir) diff --git a/i18n-generator/src/main/kotlin/dev/kordex/i18n/generator/TranslationsClass.kt b/i18n-generator/src/main/kotlin/dev/kordex/i18n/generator/TranslationsClass.kt index 358313a..0fda621 100644 --- a/i18n-generator/src/main/kotlin/dev/kordex/i18n/generator/TranslationsClass.kt +++ b/i18n-generator/src/main/kotlin/dev/kordex/i18n/generator/TranslationsClass.kt @@ -16,7 +16,11 @@ import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.PropertySpec +import dev.kordex.i18n.files.FileFormat +import dev.kordex.i18n.files.PropertiesFormat +import dev.kordex.i18n.messages.formats.ICUFormatV1 import java.io.File +import java.nio.file.Path import java.util.* public val DELIMITERS: Array = arrayOf("_", "-", ".") @@ -24,13 +28,11 @@ public val MESSAGE_FORMAT_VERSIONS: Array = arrayOf(1, 2) /** * Representation of a generated translations object. + * Handles code generation and formatting based on the given parameters. * * Usually, you can create an instance of this class and then immediately call [writeTo]. * Nothing else needs to be done for most use-cases. * - * @param allProps Properties object containing your bundle's default translations, typically loaded from a properties - * file without a locale in its filename. - * * @param bundle Name for your translation bundle, representing its location in your resources under `translations`. * For example, `core.strings` represents translations in `translations/core`, with the filenames * starting with `string.`. @@ -38,45 +40,47 @@ public val MESSAGE_FORMAT_VERSIONS: Array = arrayOf(1, 2) * @param className Name given to the generated translations object. Defaults to `Translations`. * @param classPackage Package to place the generated translations object in. * + * @param editorConfig An optional path to an editorconfig file, used to autoformat the generated code. + * Set to `null` to use the default formatting settings instead. + * Defaults to `.editorconfig` in the current working directory. + * + * @param fileFormat FileFormat object representing the file format required to load your bundle's files. + * Defaults to Java Properties format. + * + * @para messageFormat Message format identifier, representing the message format your bundle uses. + * Defaults to ICU Message Format version 1. + * * @param publicVisibility Whether to use `public` (`true`) or `internal` (`false`) visibility modifiers in the * generated code. * Defaults to (`true`). * - * @param splitToCamelCase Whether to replace common delimiters in generated names + * @param resourceBundle Resource bundle object containing your bundle's base translations. */ public class TranslationsClass( - public val allProps: Properties, + /** Constructor parameter, see [TranslationsClass]. **/ public val bundle: String, + /** Constructor parameter, see [TranslationsClass]. **/ public val className: String = "Translations", - public val publicVisibility: Boolean = true, - - @Deprecated("This option is provided for compatibility with old code, and will be removed in a future version.") - public val splitToCamelCase: Boolean = true, + /** Constructor parameter, see [TranslationsClass]. **/ public val classPackage: String, - public val messageFormatVersion: Int = 1, -) { - init { - @Suppress("DEPRECATION") - if (!splitToCamelCase) { - System.err.println("") - - System.err.println( - "WARNING: Configured to replace delimiters with underscores instead of converting names to " + - "camel-case. This option will be removed in a future version." - ) - System.err.println("") - } + /** Constructor parameter, see [TranslationsClass]. **/ + public val editorConfig: Path? = Path.of(".editorconfig"), - if (messageFormatVersion !in MESSAGE_FORMAT_VERSIONS) { - error( - "Invalid message format version $messageFormatVersion - " + - "must be one of ${MESSAGE_FORMAT_VERSIONS.joinToString()}" - ) - } - } + /** Constructor parameter, see [TranslationsClass]. **/ + public val fileFormat: FileFormat = PropertiesFormat, + + /** Constructor parameter, see [TranslationsClass]. **/ + public val messageFormat: String = ICUFormatV1.identifier, + + /** Constructor parameter, see [TranslationsClass]. **/ + public val publicVisibility: Boolean = true, + + /** Constructor parameter, see [TranslationsClass]. **/ + public val resourceBundle: ResourceBundle, +) { /** KModifier represented by [publicVisibility]. **/ public val visibility: KModifier = if (publicVisibility) { @@ -85,8 +89,8 @@ public class TranslationsClass( KModifier.INTERNAL } - /** Flat list containing all translation keys in [allProps]. **/ - public val allKeys: List = allProps.toList().map { (left, _) -> left.toString() } + /** Flat list containing all translation keys in [resourceBundle]. **/ + public val allKeys: List = resourceBundle.keys.toList() /** * KotlinPoet [FileSpec] representing the file being generated. @@ -94,15 +98,11 @@ public class TranslationsClass( * The [TranslationsClass] fills this automatically, and it is complete as soon as you've created one. */ public val spec: FileSpec = buildFileSpec(classPackage, className) { - if (messageFormatVersion != 1) { - this.addImport("dev.kordex.core.i18n.types", "MessageFormatVersion") - } - types.addObject(className) { addModifiers(visibility) bundle() - addKeys(allKeys, allProps, className) + addKeys(allKeys, className) } } @@ -116,7 +116,17 @@ public class TranslationsClass( * @param outputDir [File] representing the output directory. */ public fun writeTo(outputDir: File) { - spec.writeTo(outputDir) + val buffer = StringBuffer() + + spec.writeTo(buffer) + + val code = buffer.toString().formatCode(editorConfig) + + val parentPath = File(outputDir, classPackage.replace('.', File.separatorChar)) + val outputFile = File(parentPath, "$className.kt") + + parentPath.mkdirs() + outputFile.writeText(code) } /** @@ -142,7 +152,6 @@ public class TranslationsClass( public fun TypeSpecBuilder.addKeys( keys: List, - props: Properties, translationsClassName: String, parent: String? = null, ) { @@ -155,9 +164,9 @@ public class TranslationsClass( k } - if (v.isEmpty() || props[keyName] != null) { + if (v.isEmpty() || resourceBundle.getStringOrNull(keyName) != null) { properties.add( - key(k.toVarName(), keyName, props.getProperty(keyName), translationsClassName) + key(k.toVarName(), keyName, resourceBundle.getString(keyName), translationsClassName) ) } @@ -165,7 +174,7 @@ public class TranslationsClass( // Object types.addObject(k.toClassName()) { addModifiers(visibility) - addKeys(v, props, translationsClassName, keyName) + addKeys(v, translationsClassName, keyName) } } } @@ -176,11 +185,11 @@ public class TranslationsClass( buildPropertySpec("bundle", ClassName("dev.kordex.core.i18n.types", "Bundle")) { addModifiers(visibility) - if (messageFormatVersion != 1) { - setInitializer("Bundle(%S, %L)", bundle, messageFormatVersion.toMessageFormatEnum()) - } else { - setInitializer("Bundle(%S)", bundle) - } + setInitializer( + "Bundle(\nname = %S, \nfileFormat = %S,\nmessageFormat = %S\n)", + + bundle, fileFormat.identifiers.first(), messageFormat + ) } ) } @@ -207,27 +216,13 @@ public class TranslationsClass( @Suppress("DEPRECATION") public fun String.toVarName(): String = let { - if (splitToCamelCase) { - toClassName() - .replaceFirstChar { it.lowercase() } - } else { - it.replace("-", "_") - .replace(".", "_") - .replaceFirstChar { it.lowercase() } - } + toClassName() + .replaceFirstChar { it.lowercase() } } @Suppress("DEPRECATION", "SpreadOperator") public fun String.toClassName(): String = - let { - if (splitToCamelCase) { - it.split(*DELIMITERS).joinToString("") { it.capitalized() } - } else { - it.replace("-", " ") - .split(" ") - .joinToString("") { it.capitalized() } - } - } + split(*DELIMITERS).joinToString("") { it.capitalized() } public fun Int.toMessageFormatEnum(): String = when (this) { 1 -> "MessageFormatVersion.ONE" @@ -238,4 +233,18 @@ public class TranslationsClass( "must be one of ${MESSAGE_FORMAT_VERSIONS.joinToString()}" ) } + + public fun ResourceBundle.getStringOrNull(key: String): String? { + val result = try { + getString(key) + } catch (_: MissingResourceException) { + return null + } + + if (result == key) { + return null + } + + return result + } } diff --git a/i18n-generator/src/main/kotlin/dev/kordex/i18n/generator/_Formatting.kt b/i18n-generator/src/main/kotlin/dev/kordex/i18n/generator/_Formatting.kt new file mode 100644 index 0000000..1052a2b --- /dev/null +++ b/i18n-generator/src/main/kotlin/dev/kordex/i18n/generator/_Formatting.kt @@ -0,0 +1,42 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package dev.kordex.i18n.generator + +import com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3 +import com.pinterest.ktlint.rule.engine.api.Code +import com.pinterest.ktlint.rule.engine.api.EditorConfigDefaults +import com.pinterest.ktlint.rule.engine.api.KtLintRuleEngine +import com.pinterest.ktlint.rule.engine.core.api.AutocorrectDecision +import com.pinterest.ktlint.rule.engine.core.api.propertyTypes +import java.net.URL +import java.net.URLClassLoader +import java.nio.file.Path +import java.util.* + +private val ruleProviders = ServiceLoader.load( + RuleSetProviderV3::class.java, + URLClassLoader(emptyArray()), +) + .flatMap { it.getRuleProviders() } + .toSet() + +public fun String.formatCode(editorConfig: Path? = null): String { + val ruleEngine = KtLintRuleEngine( + ruleProviders = ruleProviders, + + editorConfigDefaults = EditorConfigDefaults.load( + path = editorConfig, + propertyTypes = ruleProviders.propertyTypes() + ) + ) + + val code = Code.fromSnippet(this) + + return ruleEngine.format(code) { + AutocorrectDecision.ALLOW_AUTOCORRECT + } +} diff --git a/i18n-generator/src/main/resources/simplelogger.properties b/i18n-generator/src/main/resources/simplelogger.properties new file mode 100644 index 0000000..2561471 --- /dev/null +++ b/i18n-generator/src/main/resources/simplelogger.properties @@ -0,0 +1,10 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +org.slf4j.simpleLogger.cacheOutputStream=true +org.slf4j.simpleLogger.defaultLogLevel=info +org.slf4j.simpleLogger.levelInBrackets=true +org.slf4j.simpleLogger.showDateTime=false +org.slf4j.simpleLogger.showShortLogName=true +org.slf4j.simpleLogger.showThreadName=false diff --git a/i18n/src/main/kotlin/dev/kordex/i18n/Bundle.kt b/i18n/src/main/kotlin/dev/kordex/i18n/Bundle.kt index 36c2f80..ec8f49f 100644 --- a/i18n/src/main/kotlin/dev/kordex/i18n/Bundle.kt +++ b/i18n/src/main/kotlin/dev/kordex/i18n/Bundle.kt @@ -6,6 +6,7 @@ package dev.kordex.i18n +import dev.kordex.i18n.files.FileFormat import dev.kordex.i18n.messages.MessageFormat import dev.kordex.i18n.messages.formats.ICUFormatV1 import dev.kordex.i18n.registries.ClassLoaderRegistry @@ -29,9 +30,12 @@ public data class Bundle( ClassLoaderRegistry.register(this) } - public fun getResourceBundleControl(): ResourceBundle.Control = + public fun getFileFormat(): FileFormat = FileFormatRegistry.getOrError(fileFormat) + public fun getResourceBundleControl(): ResourceBundle.Control = + getFileFormat().control + public fun getMessageFormatter(): MessageFormat = MessageFormatRegistry.getOrError(messageFormat) diff --git a/i18n/src/main/kotlin/dev/kordex/i18n/files/FileFormat.kt b/i18n/src/main/kotlin/dev/kordex/i18n/files/FileFormat.kt new file mode 100644 index 0000000..28d2ffe --- /dev/null +++ b/i18n/src/main/kotlin/dev/kordex/i18n/files/FileFormat.kt @@ -0,0 +1,14 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package dev.kordex.i18n.files + +import java.util.ResourceBundle + +public interface FileFormat { + public val identifiers: Set + public val control: ResourceBundle.Control +} diff --git a/i18n/src/main/kotlin/dev/kordex/i18n/files/PropertiesControl.kt b/i18n/src/main/kotlin/dev/kordex/i18n/files/PropertiesControl.kt deleted file mode 100644 index 83c8356..0000000 --- a/i18n/src/main/kotlin/dev/kordex/i18n/files/PropertiesControl.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package dev.kordex.i18n.files - -import dev.kordex.i18n.I18n -import java.util.Locale -import java.util.ResourceBundle - -public object PropertiesControl : ResourceBundle.Control() { - override fun getFormats(baseName: String?): MutableList { - if (baseName == null) { - throw NullPointerException() - } - - return FORMAT_PROPERTIES - } - - override fun getFallbackLocale(baseName: String?, locale: Locale?): Locale? { - if (baseName == null) { - throw NullPointerException() - } - - return if (locale == I18n.defaultLocale) { - null - } else { - I18n.defaultLocale - } - } -} diff --git a/i18n/src/main/kotlin/dev/kordex/i18n/files/PropertiesFormat.kt b/i18n/src/main/kotlin/dev/kordex/i18n/files/PropertiesFormat.kt new file mode 100644 index 0000000..d493dc7 --- /dev/null +++ b/i18n/src/main/kotlin/dev/kordex/i18n/files/PropertiesFormat.kt @@ -0,0 +1,39 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package dev.kordex.i18n.files + +import dev.kordex.i18n.I18n +import java.util.* + +public object PropertiesFormat : FileFormat { + override val identifiers: Set = setOf("properties") + override val control: ResourceBundle.Control = Control + + override fun toString(): String = "PropertiesFormat(${identifiers.joinToString(", ")})" + + private object Control : ResourceBundle.Control() { + override fun getFormats(baseName: String?): MutableList { + if (baseName == null) { + throw NullPointerException() + } + + return FORMAT_PROPERTIES + } + + override fun getFallbackLocale(baseName: String?, locale: Locale?): Locale? { + if (baseName == null) { + throw NullPointerException() + } + + return if (locale == I18n.defaultLocale) { + null + } else { + I18n.defaultLocale + } + } + } +} diff --git a/i18n/src/main/kotlin/dev/kordex/i18n/files/YamlFormat.kt b/i18n/src/main/kotlin/dev/kordex/i18n/files/YamlFormat.kt new file mode 100644 index 0000000..9245df9 --- /dev/null +++ b/i18n/src/main/kotlin/dev/kordex/i18n/files/YamlFormat.kt @@ -0,0 +1,17 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package dev.kordex.i18n.files + +import dev.akkinoc.util.YamlResourceBundle +import java.util.ResourceBundle + +public object YamlFormat : FileFormat { + override val identifiers: Set = setOf("yaml", "yml") + override val control: ResourceBundle.Control = YamlResourceBundle.Control + + override fun toString(): String = "YamlFormat(${identifiers.joinToString(", ")})" +} diff --git a/i18n/src/main/kotlin/dev/kordex/i18n/registries/FileFormatRegistry.kt b/i18n/src/main/kotlin/dev/kordex/i18n/registries/FileFormatRegistry.kt index a75d40e..bfb2e03 100644 --- a/i18n/src/main/kotlin/dev/kordex/i18n/registries/FileFormatRegistry.kt +++ b/i18n/src/main/kotlin/dev/kordex/i18n/registries/FileFormatRegistry.kt @@ -6,43 +6,58 @@ package dev.kordex.i18n.registries -import dev.akkinoc.util.YamlResourceBundle -import dev.kordex.i18n.files.PropertiesControl +import dev.kordex.i18n.files.FileFormat +import dev.kordex.i18n.files.PropertiesFormat +import dev.kordex.i18n.files.YamlFormat import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KotlinLogging -import java.util.ResourceBundle public object FileFormatRegistry { private val logger: KLogger = KotlinLogging.logger { } - private val formats: MutableMap = mutableMapOf() + private val formats: MutableMap = mutableMapOf() init { - register("properties", PropertiesControl) - - register("yaml", YamlResourceBundle.Control) - register("yml", YamlResourceBundle.Control) + register(PropertiesFormat) + register(YamlFormat) } - public fun get(identifier: String): ResourceBundle.Control? = + public fun get(identifier: String): FileFormat? = formats[identifier] - public fun getOrError(identifier: String): ResourceBundle.Control = + public fun getOrError(identifier: String): FileFormat = formats[identifier] ?: error("Unknown file format: $identifier") - public fun register(identifier: String, control: ResourceBundle.Control) { - formats[identifier] = control + public fun register(format: FileFormat) { + format.identifiers.forEach { identifier -> + formats[identifier] = format + } - logger.trace { "Registered file format \"$identifier\" to control $control" } + logger.trace { "Registered file format: $format (${format.identifiers.joinToString()})" } } - public fun unregister(identifier: String): ResourceBundle.Control? { + public fun unregister(identifier: String): FileFormat? { val result = formats.remove(identifier) if (result != null) { - logger.trace { "Unregistered file format \"$identifier\", was $result" } + logger.trace { "Unregistered file format for identifier \"$identifier\", was $result" } } return result } + + public fun unregister(format: FileFormat): Map { + val result = format.identifiers.associateWith { identifier -> + formats.remove(identifier) + }.filterValues { it != null } + + if (result.isNotEmpty()) { + logger.trace { "Couldn't remove non-registered file format: $format" } + } + + return result + } + + public fun getFormats(): MutableSet = + formats.keys } diff --git a/i18n/src/main/kotlin/dev/kordex/i18n/registries/MessageFormatRegistry.kt b/i18n/src/main/kotlin/dev/kordex/i18n/registries/MessageFormatRegistry.kt index 9c7771a..377d8f7 100644 --- a/i18n/src/main/kotlin/dev/kordex/i18n/registries/MessageFormatRegistry.kt +++ b/i18n/src/main/kotlin/dev/kordex/i18n/registries/MessageFormatRegistry.kt @@ -33,4 +33,7 @@ public object MessageFormatRegistry { logger.trace { "Registered message format \"${format.identifier}\" - $format" } } + + public fun getFormats(): MutableSet = + formats.keys } diff --git a/i18n/src/test/kotlin/tests/files/PropertiesTests.kt b/i18n/src/test/kotlin/tests/files/PropertiesTests.kt index 62df705..f07072e 100644 --- a/i18n/src/test/kotlin/tests/files/PropertiesTests.kt +++ b/i18n/src/test/kotlin/tests/files/PropertiesTests.kt @@ -8,34 +8,24 @@ package tests.files -import dev.kordex.i18n.files.PropertiesControl +import dev.kordex.i18n.files.PropertiesFormat import fixtures.TestConstants import io.kotest.core.spec.style.FunSpec import java.util.Locale import java.util.ResourceBundle -import java.util.ResourceBundle.Control.FORMAT_PROPERTIES class PropertiesTests : FunSpec({ - test("correct formats returned") { - val formats = PropertiesControl.getFormats(TestConstants.prefixedBaseName) - - assert(formats == FORMAT_PROPERTIES) { - "Incorrect formats returned - expected [${FORMAT_PROPERTIES.joinToString()}], " + - "got [${formats.joinToString()}]" - } - } - test("loads base file") { val englishResourceBundle = ResourceBundle.getBundle( TestConstants.prefixedBundle, Locale("en"), - PropertiesControl + PropertiesFormat.control ) val germanResourceBundle = ResourceBundle.getBundle( TestConstants.prefixedBundle, Locale("de"), - PropertiesControl + PropertiesFormat.control ) var translation = englishResourceBundle.getString("command.banana") diff --git a/i18n/src/test/kotlin/tests/files/RegistryTests.kt b/i18n/src/test/kotlin/tests/files/RegistryTests.kt index 5b2d9a9..5f4cdbe 100644 --- a/i18n/src/test/kotlin/tests/files/RegistryTests.kt +++ b/i18n/src/test/kotlin/tests/files/RegistryTests.kt @@ -6,28 +6,34 @@ package tests.files -import dev.akkinoc.util.YamlResourceBundle -import dev.kordex.i18n.files.PropertiesControl +import dev.kordex.i18n.files.FileFormat +import dev.kordex.i18n.files.PropertiesFormat +import dev.kordex.i18n.files.YamlFormat import dev.kordex.i18n.registries.FileFormatRegistry import io.kotest.core.spec.style.FunSpec import io.kotest.mpp.log import org.junit.jupiter.api.assertThrows -import java.lang.IllegalStateException +import java.util.* class RegistryTests : FunSpec({ + val customFormat = object : FileFormat { + override val identifiers: Set = setOf("test") + override val control: ResourceBundle.Control = PropertiesFormat.control + } + beforeTest { if (it.name.originalName == "functional registration") { - log { "Registering file format with identifier 'test'" } + log { "Registering file format with identifiers: ${customFormat.identifiers.joinToString()}" } - FileFormatRegistry.register("test", PropertiesControl) + FileFormatRegistry.register(customFormat) } } afterTest { if (it.a.name.originalName == "functional registration") { - log { "Unregistering file format with identifier 'test'" } + log { "Unregistering file format with identifiers: ${customFormat.identifiers.joinToString()}" } - FileFormatRegistry.unregister("test") + FileFormatRegistry.unregister(customFormat) } } @@ -36,16 +42,16 @@ class RegistryTests : FunSpec({ val yamlControlShort = FileFormatRegistry.get("yml") val yamlControlLong = FileFormatRegistry.get("yaml") - assert(propertiesControl == PropertiesControl) { - "Incorrect control returned - expected `PropertiesControl`, got $propertiesControl" + assert(propertiesControl == PropertiesFormat) { + "Incorrect control returned - expected `PropertiesFormat`, got $propertiesControl" } - assert(yamlControlShort == YamlResourceBundle.Control) { - "Incorrect control returned - expected `YamlResourceBundle.Control`, got $propertiesControl" + assert(yamlControlShort == YamlFormat) { + "Incorrect control returned - expected `YamlFormat`, got $propertiesControl" } - assert(yamlControlLong == YamlResourceBundle.Control) { - "Incorrect control returned - expected `YamlResourceBundle.Control`, got $propertiesControl" + assert(yamlControlLong == YamlFormat) { + "Incorrect control returned - expected `YamlFormat`, got $propertiesControl" } } @@ -66,8 +72,8 @@ class RegistryTests : FunSpec({ test("functional registration") { val propertiesControl = FileFormatRegistry.get("test") - assert(propertiesControl == PropertiesControl) { - "Incorrect control returned - expected `PropertiesControl`, got $propertiesControl" + assert(propertiesControl == customFormat) { + "Incorrect control returned - expected `customFormat`, got $propertiesControl" } } }) diff --git a/test/strings.yml b/test/strings.yml new file mode 100644 index 0000000..7fef073 --- /dev/null +++ b/test/strings.yml @@ -0,0 +1,20 @@ +commands: + button: + action: You pushed the button! + description: A simple example command sending a button + label: Button! + name: button + + slap: + action: slaps {0} with their {1} + description: Ask the bot to slap another user + name: slap + + args: + target: + description: Person you want to slap + name: target + + weapon: + description: What you want to slap with + name: weapon