mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-10-19 22:02:57 +02:00
The interface is German or English, chosen per operator profile on the GUI tab, in force at once in every open window, and remembered across restarts. 376 texts in both languages. The texts are ordinary .properties files that nobody has to compile: a contributor copies the English one, translates the values, and opens a pull request. A third module -- build tooling that never ships -- turns them into Kotlin before anything compiles, which is what makes a mistyped key and a wrong argument count compile errors rather than things an operator discovers. It lives in the main build rather than buildSrc because buildSrc's tests do not run on `./gradlew build`, measured, and a generator whose tests never run is the exact failure it exists to prevent. Incompleteness is allowed and incorrectness is not. A missing key warns and falls back to English, so a partial contributed language can be submitted at all; an orphan key, a placeholder the translation drops, a duplicate key or an unreadable file fail the build with the generator's own message naming the file. The argument count always comes from the English base. German is held complete by a test because it ships; a contributed language is not. What is deliberately not translated is the harder half. Protocol frames and chat commands would break the connection or go unread; the beacon texts, auto answers, snippets and shortcuts go to other radio amateurs and sit in every operator's stored profile; the 273 println calls in core are diagnostics, and a German stack trace is worse for whoever reads the bug report; and QRA, QRG, QTF, LOS, Fresnel and wkd read the same in either language. Three tests guard the boundary by measuring behaviour rather than text: the protocol frames built under three locales, every preference default compared by reflection, and no translation value being a protocol command. The reflective one is the only one that covers what nobody thought of. One insight turned up three times. Anything built outside a composition freezes the language it was built in: the window title, the settings tab strip and every column header are all constructed once at startup -- the columns from Java -- so each now holds a function read when it is drawn. That also fixed something older and unrelated to language: the main window's title was a fixed "KST4Contest (Compose)" for the whole session, where the JavaFX window had carried the live connection line. It carries it again. A column's id is never translated. It is what the remembered widths and order are keyed on.
158 lines
7.0 KiB
Kotlin
158 lines
7.0 KiB
Kotlin
plugins {
|
|
java
|
|
alias(libs.plugins.kotlinJvm)
|
|
alias(libs.plugins.composeCompiler)
|
|
alias(libs.plugins.composeMultiplatform)
|
|
}
|
|
|
|
kotlin {
|
|
jvmToolchain(21)
|
|
}
|
|
|
|
dependencies {
|
|
// Mirrors the compile-scope entries in pom.xml: a few test classes still live under
|
|
// src/main/java and need JUnit and Mockito on the main compile classpath.
|
|
implementation(libs.junit.jupiter.api)
|
|
implementation(libs.mockito.core.compile)
|
|
implementation(libs.jetbrains.annotations)
|
|
implementation(project(":core"))
|
|
implementation(compose.desktop.currentOs)
|
|
implementation(compose.material3)
|
|
implementation(libs.sqlite.jdbc)
|
|
implementation(libs.jlayer)
|
|
/*
|
|
* The Compose UI test harness, used through runComposeUiTest { } from ordinary Jupiter
|
|
* tests. Deliberately NOT ui-test-junit4: that one brings the vintage engine, and two
|
|
* engines on one classpath is how half a suite silently stops being run. Checked after
|
|
* adding this: the only engine resolved is junit-jupiter-engine. JUnit 4 classes are on
|
|
* the test classpath, but they were before this too — something else pulls them
|
|
* transitively — and with no vintage engine nothing executes them.
|
|
*
|
|
* The accessor is marked experimental by the Compose Gradle plugin, so the opt-in sits
|
|
* here — one place, for one dependency. What it buys is the only mechanism that can
|
|
* answer "did the value reach the screen": the state-level tests and the two settings
|
|
* coverage nets check that a setter is called, which is a different question.
|
|
*/
|
|
@OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
|
|
testImplementation(compose.uiTest)
|
|
testImplementation(libs.junit.jupiter)
|
|
testImplementation(libs.mockito.core)
|
|
testImplementation(libs.mockito.junit.jupiter)
|
|
// Gradle 9 no longer adds the launcher implicitly.
|
|
testRuntimeOnly(libs.junit.platform.launcher)
|
|
}
|
|
|
|
/*
|
|
* The translation files are turned into Kotlin before anything compiles, by the
|
|
* :i18n-generator module. Two consequences worth naming:
|
|
*
|
|
* - A broken translation fails `./gradlew classes`, which is what the PR workflow already
|
|
* runs -- so a contributor's mistake is reported in their pull request without a new
|
|
* workflow existing for it.
|
|
* - The generator is reached through its own configuration, not through `implementation`, so
|
|
* it stays out of the application's runtime classpath and out of the jar.
|
|
*/
|
|
val i18nGenerator: Configuration by configurations.creating
|
|
|
|
dependencies {
|
|
i18nGenerator(project(":i18n-generator"))
|
|
}
|
|
|
|
val generatedI18nDirectory = layout.buildDirectory.dir("generated/i18n")
|
|
|
|
val generateStrings by tasks.registering(JavaExec::class) {
|
|
description = "Generates Strings.kt from app-desktop/src/main/i18n"
|
|
group = "build"
|
|
|
|
classpath = i18nGenerator
|
|
mainClass.set("kst4contest.i18n.GeneratorMain")
|
|
|
|
val input = layout.projectDirectory.dir("src/main/i18n")
|
|
inputs.dir(input)
|
|
outputs.dir(generatedI18nDirectory)
|
|
|
|
argumentProviders.add(CommandLineArgumentProvider {
|
|
listOf(input.asFile.absolutePath, generatedI18nDirectory.get().asFile.absolutePath)
|
|
})
|
|
}
|
|
|
|
kotlin.sourceSets["main"].kotlin.srcDir(generatedI18nDirectory)
|
|
|
|
tasks.named("compileKotlin") { dependsOn(generateStrings) }
|
|
|
|
compose.desktop {
|
|
application {
|
|
// Main forwards to Kst4ContestApplication.main. It was a workaround for
|
|
// launching a JavaFX Application subclass from the classpath; it stays because
|
|
// the packaged launchers and the AUR/Flatpak wrappers name this class.
|
|
mainClass = "kst4contest.view.Main"
|
|
|
|
nativeDistributions {
|
|
/*
|
|
* Only the formats the building machine can actually produce. Compose rejects a
|
|
* foreign one while CONFIGURING the project, not while packaging it, so a list
|
|
* naming all five made every Gradle task fail on macOS and Windows — including
|
|
* `run` and `test`, which package nothing. The build was Linux-only without
|
|
* anyone having decided that.
|
|
*/
|
|
val host = org.gradle.internal.os.OperatingSystem.current()
|
|
targetFormats(
|
|
*when {
|
|
host.isMacOsX -> arrayOf(
|
|
org.jetbrains.compose.desktop.application.dsl.TargetFormat.Dmg,
|
|
)
|
|
host.isWindows -> arrayOf(
|
|
org.jetbrains.compose.desktop.application.dsl.TargetFormat.Msi,
|
|
)
|
|
else -> arrayOf(
|
|
org.jetbrains.compose.desktop.application.dsl.TargetFormat.AppImage,
|
|
org.jetbrains.compose.desktop.application.dsl.TargetFormat.Deb,
|
|
org.jetbrains.compose.desktop.application.dsl.TargetFormat.Rpm,
|
|
)
|
|
}
|
|
)
|
|
/*
|
|
* jlink must come from JDK 21, not from whatever the Gradle JVM is.
|
|
* jdk.jsobject was removed after 21, and this machine has 26 on PATH;
|
|
* Etappe 1 hit the same wall with jpackage.
|
|
*/
|
|
javaHome = javaToolchains.launcherFor {
|
|
languageVersion.set(JavaLanguageVersion.of(21))
|
|
}.get().metadata.installationPath.asFile.absolutePath
|
|
|
|
/*
|
|
* KST4Contest and not praktiKST: this one name drives the app-image
|
|
* directory, the launcher, the .app bundle, the bundled icon file
|
|
* and -- lowercased -- the deb/rpm package name. Every packaging
|
|
* consumer already uses KST4Contest there (AppRun, the Arch and
|
|
* Flatpak wrappers, the AUR PKGBUILDs, build-signed-dmg.sh), and the
|
|
* shipped deb/rpm must keep the package identity "kst4contest" so an
|
|
* installed copy still upgrades.
|
|
*/
|
|
packageName = "KST4Contest"
|
|
packageVersion = providers.gradleProperty("composePackageVersion").get()
|
|
|
|
/*
|
|
* Kept from the former module-info.java. jdk.jsobject went with the map
|
|
* bridge: it carried netscape.javascript, and nothing imports that any
|
|
* more. jdk.unsupported stays — its justification used to be the JavaFX
|
|
* Marlin renderer, but sun.misc.Unsafe is reached by other dependencies
|
|
* too, and dropping it is a separate decision from removing JavaFX.
|
|
*/
|
|
modules(
|
|
"java.desktop", "java.net.http", "java.sql",
|
|
"jdk.crypto.ec", "jdk.net",
|
|
"jdk.xml.dom", "jdk.unsupported",
|
|
)
|
|
|
|
linux { iconFile.set(rootProject.file("packaging/icons/kst4contest.png")) }
|
|
windows { iconFile.set(rootProject.file("packaging/icons/kst4contest.ico")) }
|
|
macOS {
|
|
iconFile.set(rootProject.file("packaging/icons/kst4contest.icns"))
|
|
// Must stay de.x08.KST4Contest: build-signed-dmg.sh signs against it.
|
|
bundleID = "de.x08.KST4Contest"
|
|
}
|
|
}
|
|
}
|
|
}
|