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>
This commit is contained in:
@@ -1,9 +1,92 @@
|
||||
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
|
||||
@@ -15,11 +98,14 @@ kotlin {
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation(project(":shared"))
|
||||
commonMain {
|
||||
kotlin.srcDir(generateStrings)
|
||||
dependencies {
|
||||
implementation(project(":shared"))
|
||||
|
||||
implementation(libs.compose.runtime)
|
||||
implementation(libs.compose.html.core)
|
||||
implementation(libs.compose.runtime)
|
||||
implementation(libs.compose.html.core)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user