Strings.kt is now generated at build time (:webHtmlApp:generateStrings, in webHtmlApp/build.gradle.kts) by parsing shared's strings.xml directly, rather than a hand-maintained duplicate that could silently drift from it. Names are mechanical camelCase of the XML key, so a few call sites got more verbose (e.g. moveUp -> moveCurrencyUpContentDescription) in exchange for zero manual sync. Only covers the base English locale. Also collapses currency_option_label_local into currency_option_label: the two had identical English text and existed only as a hook for a future translation to phrase the user's own currency differently, which no locale currently does. Removes the info.code == localeCurrencyCode branching (and the now-pointless localeCurrencyCode parameter/field) from all three UI implementations - PriceScreen.kt, CurrencyPickerSheet.swift, and HtmlCurrencyPicker.kt - plus IosConverterState.localeCurrencyCode, which existed solely to feed that branch on the Swift side. Fixes a real regression surfaced while verifying the above by actually building the Xcode project (not done since currencyFlagEmoji() gained its supportsFlagEmoji parameter): Kotlin/Native doesn't generate default-parameter overloads for Swift/ObjC-exported functions, so both Swift call sites needed the argument passed explicitly, via the correct exported facade name for an expect/actual pair - the actual file's (SystemCurrencies_appleKt), not the common one (SystemCurrenciesKt). Also drops "(DOM)" from webHtmlApp's page title. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
112 lines
4.9 KiB
Kotlin
112 lines
4.9 KiB
Kotlin
import org.w3c.dom.Element
|
|
import javax.xml.parsers.DocumentBuilderFactory
|
|
|
|
plugins {
|
|
alias(libs.plugins.kotlinMultiplatform)
|
|
alias(libs.plugins.composeMultiplatform)
|
|
alias(libs.plugins.composeCompiler)
|
|
}
|
|
|
|
// webHtmlApp can't use moko-resources' Compose integration (stringResource()) without pulling
|
|
// in compose.foundation/ui transitively - exactly what this module exists to avoid (see
|
|
// AGENTS.md's "Module layout"). Generating a plain Strings.kt from the same strings.xml moko
|
|
// reads keeps that file as the single source of truth without a manually maintained, silently
|
|
// driftable duplicate. Only covers the base (English) locale - see AGENTS.md.
|
|
val stringsXmlFile = rootProject.file("shared/src/commonMain/moko-resources/base/strings.xml")
|
|
|
|
val generateStrings by tasks.registering {
|
|
val outputDir = layout.buildDirectory.dir("generated/strings")
|
|
// Captured as a plain local (rather than read from the doLast closure directly) so the task
|
|
// action doesn't hold a reference to the build script object itself, which the configuration
|
|
// cache refuses to serialize.
|
|
val xmlFile = stringsXmlFile
|
|
inputs.file(xmlFile)
|
|
outputs.dir(outputDir)
|
|
|
|
doLast {
|
|
val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(xmlFile)
|
|
val stringNodes = document.getElementsByTagName("string")
|
|
val placeholderPattern = Regex("""%(\d+)\$([sd])""")
|
|
|
|
fun String.toCamelCase(): String = split("_").mapIndexed { index, word ->
|
|
if (index == 0) word else word.replaceFirstChar { it.uppercase() }
|
|
}.joinToString("")
|
|
|
|
val source = buildString {
|
|
appendLine("// GENERATED FILE - do not edit directly.")
|
|
appendLine("// Regenerated by :webHtmlApp:generateStrings from")
|
|
appendLine("// shared/src/commonMain/moko-resources/base/strings.xml - edit that file instead.")
|
|
appendLine("package xyz.tyiu.satsprice")
|
|
appendLine()
|
|
appendLine("object Strings {")
|
|
for (i in 0 until stringNodes.length) {
|
|
val element = stringNodes.item(i) as Element
|
|
val propertyName = element.getAttribute("name").toCamelCase()
|
|
// moko/Android string-resource XML escapes a literal apostrophe as \' inside the
|
|
// (unquoted) text content - unescape that before anything else so it doesn't get
|
|
// mistaken for a Kotlin escape sequence below.
|
|
val rawValue = element.textContent.replace("\\'", "'")
|
|
|
|
val placeholders = placeholderPattern.findAll(rawValue)
|
|
.map { it.groupValues[1].toInt() to it.groupValues[2] }
|
|
.distinct()
|
|
.sortedBy { it.first }
|
|
.toList()
|
|
|
|
// Swap each %N$s/%N$d placeholder for a control-character sentinel first, so the
|
|
// literal '$' in "%1$s" itself doesn't get caught by the blanket '$' -> "\$"
|
|
// escaping below - then escape, then turn the sentinels into "${argN}".
|
|
var withSentinels = rawValue
|
|
for ((index, _) in placeholders) {
|
|
withSentinels = withSentinels.replace(Regex("""%$index\$[sd]"""), "$index")
|
|
}
|
|
val escaped = withSentinels
|
|
.replace("\\", "\\\\")
|
|
.replace("\"", "\\\"")
|
|
.replace("$", "\\$")
|
|
var templateBody = escaped
|
|
for ((index, _) in placeholders) {
|
|
templateBody = templateBody.replace("$index", "\${arg$index}")
|
|
}
|
|
|
|
if (placeholders.isEmpty()) {
|
|
appendLine(" const val $propertyName: String = \"$templateBody\"")
|
|
} else {
|
|
val params = placeholders.joinToString(", ") { (index, type) ->
|
|
"arg$index: ${if (type == "d") "Int" else "String"}"
|
|
}
|
|
appendLine(" fun $propertyName($params): String = \"$templateBody\"")
|
|
}
|
|
}
|
|
appendLine("}")
|
|
}
|
|
|
|
val outputFile = outputDir.get().file("xyz/tyiu/satsprice/Strings.kt").asFile
|
|
outputFile.parentFile.mkdirs()
|
|
outputFile.writeText(source)
|
|
}
|
|
}
|
|
|
|
// Compose HTML (org.jetbrains.compose.html:html-core) only publishes js and jvm target
|
|
// variants as of Compose Multiplatform 1.11.1 - no wasmJs artifact exists yet, unlike the
|
|
// Skia-based Compose Multiplatform UI toolkit webApp uses (which targets both). So this
|
|
// comparison module is js-only for now.
|
|
kotlin {
|
|
js {
|
|
browser()
|
|
binaries.executable()
|
|
}
|
|
|
|
sourceSets {
|
|
commonMain {
|
|
kotlin.srcDir(generateStrings)
|
|
dependencies {
|
|
implementation(project(":shared"))
|
|
|
|
implementation(libs.compose.runtime)
|
|
implementation(libs.compose.html.core)
|
|
}
|
|
}
|
|
}
|
|
}
|