diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9976217..30f057b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -24,16 +24,16 @@ jobs: runs-on: macos-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: oracle java-version: '26' - name: Set up Gradle - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v5 - name: Build run: ./gradlew clean build @@ -51,16 +51,16 @@ jobs: ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_IN_MEMORY_KEY_PASSWORD }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: oracle java-version: '26' - name: Set up Gradle - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v5 - name: Check release version run: | diff --git a/README.md b/README.md index 042dbb5..d62ca1b 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ Apply the plugin after the Kotlin JVM plugin in Kotlin modules: ```kotlin plugins { kotlin("jvm") - id("no.beint.thim") version "0.5.0" + id("no.beint.thim") version "0.5.1" } ``` @@ -54,7 +54,7 @@ Java modules need only the Java and Thim plugins: ```kotlin plugins { java - id("no.beint.thim") version "0.5.0" + id("no.beint.thim") version "0.5.1" } ``` diff --git a/build.gradle.kts b/build.gradle.kts index 50d911e..af4b682 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -15,7 +15,7 @@ plugins { allprojects { group = "no.beint.thim" - version = "0.5.0" + version = "0.5.1" } subprojects { diff --git a/compiler/src/main/kotlin/no/beint/thim/compiler/MessageCatalog.kt b/compiler/src/main/kotlin/no/beint/thim/compiler/MessageCatalog.kt index 05d0075..f778161 100644 --- a/compiler/src/main/kotlin/no/beint/thim/compiler/MessageCatalog.kt +++ b/compiler/src/main/kotlin/no/beint/thim/compiler/MessageCatalog.kt @@ -110,23 +110,27 @@ internal class MessageCatalog private constructor( } if (!Files.exists(directory)) return MessageCatalog(emptyMap(), canonicalDefault, canonicalSupported) require(Files.isDirectory(directory)) { "Message catalog directory does not exist: $directory" } - val shortExtensions = Files.walk(directory).use { paths -> - paths.filter { Files.isRegularFile(it) && it.extension == "yml" } - .map(Path::toString) + val catalogFiles = Files.walk(directory).use { paths -> + paths.filter { + Files.isRegularFile(it) && it.extension.lowercase(Locale.ROOT) in setOf("yaml", "yml") + } .sorted() .toList() } - require(shortExtensions.isEmpty()) { - "Message catalogs must use the .yaml extension, found $shortExtensions" + val invalidExtensions = catalogFiles.filter { it.extension != "yaml" } + require(invalidExtensions.isEmpty()) { + "Message catalogs must use the .yaml extension, found $invalidExtensions" } - - val discovered = Files.list(directory).use { paths -> - paths.filter { Files.isDirectory(it) } - .filter { localeDirectory -> containsYaml(localeDirectory) } - .map { it.name } - .sorted() - .toList() + val yamlFiles = catalogFiles.filter { it.extension == "yaml" } + val misplaced = yamlFiles.filter { directory.relativize(it).nameCount < 2 } + require(misplaced.isEmpty()) { + "Message catalogs must be stored inside a locale directory, found $misplaced" } + + val discovered = yamlFiles + .map { directory.relativize(it).getName(0).toString() } + .distinct() + .sorted() val unexpected = discovered - canonicalSupported.toSet() require(unexpected.isEmpty()) { "Message catalog has unsupported locale directories $unexpected" } @@ -404,10 +408,6 @@ internal class MessageCatalog private constructor( return canonical } - private fun containsYaml(directory: Path): Boolean = Files.walk(directory).use { paths -> - paths.anyMatch { Files.isRegularFile(it) && it.extension == "yaml" } - } - private fun suggestion(value: String, candidates: Collection): String { val nearest = candidates.minByOrNull { distance(value, it) } ?: return "" return if (distance(value, nearest) <= 2) "; did you mean '$nearest'?" else "" diff --git a/compiler/src/main/kotlin/no/beint/thim/compiler/RendererGenerator.kt b/compiler/src/main/kotlin/no/beint/thim/compiler/RendererGenerator.kt index b047427..ff5b6cb 100644 --- a/compiler/src/main/kotlin/no/beint/thim/compiler/RendererGenerator.kt +++ b/compiler/src/main/kotlin/no/beint/thim/compiler/RendererGenerator.kt @@ -17,11 +17,12 @@ internal data class CompiledTemplate( internal class StaticContent { private val output = ByteArrayOutputStream() + private val ranges = hashMapOf() - fun append(value: String): IntRange { + fun append(value: String): IntRange = ranges.getOrPut(value) { val start = output.size() output.writeBytes(value.toByteArray(StandardCharsets.UTF_8)) - return start until output.size() + start until output.size() } fun bytes(): ByteArray = output.toByteArray() @@ -289,7 +290,7 @@ internal class RendererGenerator( } renderErrors(errorsAttribute, element, scope, code) } else if (fieldExpansion?.content != null) { - code.statement("output.text(${fieldExpansion?.content});") + code.statement("output.text(${fieldExpansion.content});") } else if (text == null && safeHtml == null) { element.children.forEach { renderNodeCollecting(it, scope, code, context) } } else if (safeHtml != null) { diff --git a/compiler/src/test/kotlin/no/beint/thim/compiler/MessageCatalogTest.kt b/compiler/src/test/kotlin/no/beint/thim/compiler/MessageCatalogTest.kt index 22c74a6..12b71fa 100644 --- a/compiler/src/test/kotlin/no/beint/thim/compiler/MessageCatalogTest.kt +++ b/compiler/src/test/kotlin/no/beint/thim/compiler/MessageCatalogTest.kt @@ -139,6 +139,22 @@ class MessageCatalogTest { } } + @Test + fun `rejects case variant extensions and catalogs outside locale directories`() { + write("en/home.yaml", "title: Hello") + write("en/extra.YAML", "subtitle: Welcome") + + assertProblem("must use the .yaml extension") { + MessageCatalog.load(directory, "en", listOf("en")) + } + + Files.delete(directory.resolve("en/extra.YAML")) + write("root.yaml", "title: Ignored") + assertProblem("must be stored inside a locale directory") { + MessageCatalog.load(directory, "en", listOf("en")) + } + } + @Test fun `requires the same keys and argument contract in every locale`() { write("en/home.yaml", "title: Hello {name}\nsubtitle: Welcome") diff --git a/compiler/src/test/kotlin/no/beint/thim/compiler/StaticContentTest.kt b/compiler/src/test/kotlin/no/beint/thim/compiler/StaticContentTest.kt new file mode 100644 index 0000000..8a20a67 --- /dev/null +++ b/compiler/src/test/kotlin/no/beint/thim/compiler/StaticContentTest.kt @@ -0,0 +1,22 @@ +package no.beint.thim.compiler + +import java.nio.charset.StandardCharsets +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals + +class StaticContentTest { + @Test + fun `reuses the byte range for identical static content`() { + val content = StaticContent() + + val first = content.append("Save") + val second = content.append("Cancel") + val repeated = content.append("Save") + + assertEquals(first, repeated) + assertEquals(0 until 4, first) + assertEquals(4 until 10, second) + assertContentEquals("SaveCancel".toByteArray(StandardCharsets.UTF_8), content.bytes()) + } +} diff --git a/gradle-plugin/src/main/java/no/beint/thim/gradle/ThimPlugin.java b/gradle-plugin/src/main/java/no/beint/thim/gradle/ThimPlugin.java index 493db0a..e6abe8a 100644 --- a/gradle-plugin/src/main/java/no/beint/thim/gradle/ThimPlugin.java +++ b/gradle-plugin/src/main/java/no/beint/thim/gradle/ThimPlugin.java @@ -87,7 +87,7 @@ private void configureKotlinProject(Project project, ThimExtension extension) { task.getInputs().files(extension.getTemplates().map(directory -> htmlFiles(project, directory.getAsFile()))) .withPropertyName("thimTemplates") .withPathSensitivity(PathSensitivity.RELATIVE); - task.getInputs().files(extension.getMessages().map(directory -> yamlFiles(project, directory.getAsFile()))) + task.getInputs().files(extension.getMessages().map(directory -> catalogFiles(project, directory.getAsFile()))) .withPropertyName("thimMessages") .withPathSensitivity(PathSensitivity.RELATIVE); }); @@ -207,8 +207,8 @@ private FileTree htmlFiles(Project project, java.io.File directory) { return project.fileTree(directory, files -> files.include("**/*.html")); } - private FileTree yamlFiles(Project project, java.io.File directory) { - return project.fileTree(directory, files -> files.include("**/*.yaml")); + private FileTree catalogFiles(Project project, java.io.File directory) { + return project.fileTree(directory, files -> files.include("**/*.yaml", "**/*.yml", "**/*.YAML", "**/*.YML")); } private String generatedPackage(Project project) {