Files
sats-price/webHtmlApp/build.gradle.kts
T
tyiuandClaude Sonnet 5 8057651688 Generate webHtmlApp's strings, collapse a redundant string key
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>
2026-09-17 07:33:47 +03:00

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)
}
}
}
}