Starting a translation no longer creates one. It opens a signing session over a TranslationArtifactVersionEvent and a TranslationChapterEvent per chapter, and the translation appears -- on every member's device at once, authored by the room's shared key rather than by whoever picked the dialect -- when enough members have signed. The same trade the dialects, artifacts and chapters made: a submission says "I am putting this in front of the group" and the group's only recourse afterwards is social, while a signature is the group saying it and it takes a quorum to say. A translation is what the group's readers will read the work as, so the second is the honest one. **The chapters, in the same batch.** A translation with no chapters is one nobody can start: a translated chunk hangs off a translation chapter, which hangs off the translation. They travel as their own signed events for the same reason the chapter's chunks do since2e133dd-- a row that carries the group's signature over its own id can be checked by anybody holding it, rather than only by whoever re-derives it. That makes this the second caller of `proposeSigningBatch`'s lead/dependents form, and for exactly the reason the form exists. A translation chapter carries the id of the translation it belongs to, and that id is a hash over the group's key at the room's derivation path -- neither resolved until the proposal runs. A caller computing it would be recomputing `signingPath`, the one input in this protocol that must never come from a proposer. So the translation is built first and handed to `AddTranslationArtifactVersionViewModel.translationChaptersOf`, which lays the chapters out against it. An item naming a translation nobody signed is not a mistake that can be made rather than one to be tested for. The lead is item 0 and items apply in `itemIndex` order, which is what MantraTranslationChapter's foreign key to MantraTranslationArtifactVersion needs: what is referenced is signed first as well as named first. **Only existing dialects.** The form's "New dialect" chip and its three fields are gone, and with them the path that created a dialect on the way to using it. A dialect is the group's too -- AddDialectScreen has proposed one for signing since the dialects made this trade -- so minting one as a side effect of translating would have put a dialect nobody agreed to underneath a translation the group did. Nothing is selected to begin with, because picking a default would be choosing the language of the work; the room's own detail screen is where a missing dialect is asked for, and the form says so when there are none. **The cost, in front of whoever is looking.** MAX_BATCH_SIZE is 64 and the translation takes one place, so MAX_CHAPTERS_PER_TRANSLATION is 63. Unlike a chapter's paragraphs this is not something the person at the screen can shorten by splitting anything, so it is stated rather than advised: the count is shown against the cap as chapters load, coloured when past it, and the form will not propose -- because the alternative is an IllegalArgumentException after the fact. The view model refuses independently; the screen is not what enforces it. **What a chapter signed later does not reach.** A translation covers the chapters that existed when it was proposed, and nothing scaffolds a translation chapter for one signed into the artifact afterwards. That is not new -- the DAO this replaces scaffolded once too -- but it is now said where somebody can act on it, in the form's own line, rather than discovered as a chapter that cannot be translated. Fixing it properly is its own change. **What went away.** MantraDao.addTranslationArtifactVersion and its way up through the repository, including the commented-out chunk scaffolding it had been carrying. Nothing called it once the screen proposed instead, and leaving a path that authors a translation under a member's key while the UI insists on a quorum is the trapeb34c8eremoved for chapters. MantraRepository.addDialect and DatabaseMantraRepository.addDialect went with it: the new-dialect path was their last caller, so what remained was a member-authored dialect reachable from any screen that holds a MantraRepository. MantraDao.addDialect stays, because MantraDaoJvmTest uses it as the worked example for the `rumorOf` seam that addArtifact, addArtifactVersion and saveTranslation still run through. **The screens.** AddTranslationArtifactVersionScreen loads the artifact's latest version, its chapters and canSign up front and disables the FAB when any of them is missing, the way the dialect, artifact and chapter screens do; on success it lands on the session rather than on an artifact the translation is not in yet. FrostSigningScreen described both new kinds as "Event of kind 30306" and a run of "Event of kind 30308", which is a member being asked to sign a translation they cannot read; it now reads the dialect name, visibility and licence for the translation and the position for each chapter. **Tests.** TranslationBatchProposalJvmTest runs the real proposal against a real database over a real ceremony, which is where the sharp edge is: item order, every chapter naming the translation as the group will author it, the source chapter and position each stands in for, one timestamp across the batch, and both ends of the cap -- 63 chapters proposes, 64 is refused and leaves no session behind. Checked against broken implementations rather than only against a working one: naming the artifact version instead of the signed translation, taking the index from list position, and stamping the chapters off their own clock are each caught. If the first of those came apart the translation would still be signed and every chapter would still verify -- against a translation id nobody has. They would fail a foreign key on the way in and the translation would simply arrive empty, which is the failure worth a database to catch. 392 jvmTest and 244 testDebugUnitTest pass, none of the existing ones edited. Not covered: applyInnerEvent's upserts, which need a database no test here stands up, and addTranslation itself, which is plumbing across two dispatchers over a template and a builder the tests already pin. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This is a Kotlin Multiplatform project targeting Android, iOS, Desktop (JVM).
-
/composeApp is for code that will be shared across your Compose Multiplatform applications. It contains several subfolders:
- commonMain is for code that’s common for all targets.
- Other folders are for Kotlin code that will be compiled for only the platform indicated in the folder name. For example, if you want to use Apple’s CoreCrypto for the iOS part of your Kotlin app, the iosMain folder would be the right place for such calls. Similarly, if you want to edit the Desktop (JVM) specific part, the jvmMain folder is the appropriate location.
-
/iosApp contains iOS applications. Even if you’re sharing your UI with Compose Multiplatform, you need this entry point for your iOS app. This is also where you should add SwiftUI code for your project.
Build and Run Android Application
To build and run the development version of the Android app, use the run configuration from the run widget in your IDE’s toolbar or build it directly from the terminal:
- on macOS/Linux
./gradlew :composeApp:assembleDebug - on Windows
.\gradlew.bat :composeApp:assembleDebug
Build and Run Desktop (JVM) Application
To build and run the development version of the desktop app, use the run configuration from the run widget in your IDE’s toolbar or run it directly from the terminal:
- on macOS/Linux
./gradlew :composeApp:run - on Windows
.\gradlew.bat :composeApp:run
Build and Run iOS Application
To build and run the development version of the iOS app, use the run configuration from the run widget in your IDE’s toolbar or open the /iosApp directory in Xcode and run it from there.
Learn more about Kotlin Multiplatform…