Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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: |
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
```

Expand All @@ -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"
}
```

Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ plugins {

allprojects {
group = "no.beint.thim"
version = "0.5.0"
version = "0.5.1"
}

subprojects {
Expand Down
32 changes: 16 additions & 16 deletions compiler/src/main/kotlin/no/beint/thim/compiler/MessageCatalog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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" }

Expand Down Expand Up @@ -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>): String {
val nearest = candidates.minByOrNull { distance(value, it) } ?: return ""
return if (distance(value, nearest) <= 2) "; did you mean '$nearest'?" else ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ internal data class CompiledTemplate(

internal class StaticContent {
private val output = ByteArrayOutputStream()
private val ranges = hashMapOf<String, IntRange>()

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()
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down Expand Up @@ -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) {
Expand Down