<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Touko Ursin</title>
    <description>The latest articles on DEV Community by Touko Ursin (@touko_ursin_77fed0aa92ca0).</description>
    <link>https://dev.to/touko_ursin_77fed0aa92ca0</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4138451%2F1a2c4dbc-9236-454b-bc8b-848a45845825.png</url>
      <title>DEV Community: Touko Ursin</title>
      <link>https://dev.to/touko_ursin_77fed0aa92ca0</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/touko_ursin_77fed0aa92ca0"/>
    <language>en</language>
    <item>
      <title>Patchwork: translations that know when they're out of date, published through Sanity</title>
      <dc:creator>Touko Ursin</dc:creator>
      <pubDate>Wed, 23 Sep 2026 02:32:26 +0000</pubDate>
      <link>https://dev.to/touko_ursin_77fed0aa92ca0/patchwork-translations-that-know-when-theyre-out-of-date-published-through-sanity-n49</link>
      <guid>https://dev.to/touko_ursin_77fed0aa92ca0/patchwork-translations-that-know-when-theyre-out-of-date-published-through-sanity-n49</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/sanity-2026-09-16"&gt;Sanity Challenge&lt;/a&gt;: Path Two, a Next.js app with Sanity as the backend.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Patchwork&lt;/strong&gt; is an editorial workbench for multilingual museum stories.&lt;/p&gt;

&lt;p&gt;A museum fixes an entrance description in English. The Finnish and Spanish pages still give yesterday's directions, and the "translation complete" checkbox still says everything is fine. Patchwork models that dependency instead of hiding it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every language keeps its own revision and &lt;strong&gt;the English revision it was based on&lt;/strong&gt;. Edit English and the translations are marked stale until someone updates them or confirms they still hold.&lt;/li&gt;
&lt;li&gt;Every story is grounded in versioned &lt;strong&gt;source notes&lt;/strong&gt; (curator notes, access walks). Correct a source and every approval that relied on it clears.&lt;/li&gt;
&lt;li&gt;A review records a &lt;strong&gt;fingerprint of the exact content, language, English revision and source versions&lt;/strong&gt;. Change any of them and the approval no longer counts.&lt;/li&gt;
&lt;li&gt;Publishing needs fresh approvals in all three languages and writes a &lt;strong&gt;separate public edition&lt;/strong&gt;. Later draft edits, source corrections and history restores never touch that edition until someone publishes again.&lt;/li&gt;
&lt;li&gt;Four mechanical gates block review: missing image description, missing summary, missing access note, and vague link text such as "click here". They are editorial prompts, not accessibility certification.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The museum ("Fieldnote Museum"), its three stories, translations and access measurements are synthetic fixtures written for the demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/yUKmSoR3Rdw" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;The film (2:08) is the actual app running locally and writing to the real Sanity project. Every save, approval and publish in it is a real transaction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live dataset:&lt;/strong&gt; project ID &lt;strong&gt;&lt;code&gt;f6kx9jxa&lt;/code&gt;&lt;/strong&gt;, public dataset &lt;strong&gt;&lt;code&gt;production&lt;/code&gt;&lt;/strong&gt;. Anyone can query the published editions, no token needed:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://f6kx9jxa.api.sanity.io/v2026-09-21/data/query/production?query=*%5B_type%20%3D%3D%20%22patchworkPublication%22%5D" rel="noopener noreferrer"&gt;https://f6kx9jxa.api.sanity.io/v2026-09-21/data/query/production?query=*%5B_type%20%3D%3D%20%22patchworkPublication%22%5D&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It currently returns two trilingual editions (the loom and the garden). Drafts, sources and receipts live under &lt;code&gt;drafts.&lt;/code&gt; and are not returned to anonymous queries; you can check with &lt;code&gt;count(*[_id in path("drafts.**")])&lt;/code&gt;, which returns 0.&lt;/p&gt;

&lt;p&gt;Review dialog: the approval is bound to a fingerprint of the exact versions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz2n33branuhyrc18spto.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz2n33branuhyrc18spto.png" alt="Review dialog showing the Finnish draft, two explicit confirmations and a version fingerprint" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All three reviews current, published:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flx9doz939t29jwvcr2yy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flx9doz939t29jwvcr2yy.png" alt="Workbench showing the garden story with three reviewed languages and a public edition" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After an English edit: Finnish and Spanish trail the source, and the published edition stays as it was:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F941mzuepffcl9rqjhf98.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F941mzuepffcl9rqjhf98.png" alt="Finnish tab flagged as based on English v1 while English is v2" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The visitor page reads only Sanity's published perspective:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcgk1adp3hdchwce5j4zo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcgk1adp3hdchwce5j4zo.png" alt="Public editions page showing the garden story" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Used Sanity
&lt;/h2&gt;

&lt;p&gt;Sanity is the whole persistence layer: the editorial state machine lives in Content Lake, and the Next.js server talks to it with the official &lt;code&gt;@sanity/client&lt;/code&gt; (API version &lt;code&gt;2026-09-21&lt;/code&gt;, &lt;code&gt;useCdn: false&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schema.&lt;/strong&gt; Five document types (Studio schema in &lt;code&gt;studio/schemaTypes&lt;/code&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;patchworkEntry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A story: stage, per-language content, revision and English base revision&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;patchworkSource&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Versioned curatorial and access evidence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;patchworkRevision&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Receipt per command: operation id, command hash, actor, reason, before/after&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;patchworkManifest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Document inventory and the app-wide concurrency lock&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;patchworkPublication&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The approved trilingual edition that visitors read&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Drafts vs published.&lt;/strong&gt; Entries, sources, receipts and the manifest are stored with &lt;code&gt;drafts.&lt;/code&gt; ids, so the public dataset does not expose them. Publications use plain ids. The authoring server reads with the authenticated &lt;code&gt;raw&lt;/code&gt; perspective; the &lt;code&gt;/visit&lt;/code&gt; page and anyone on the internet read the &lt;code&gt;published&lt;/code&gt; perspective.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One transaction per command, with revision preconditions.&lt;/strong&gt; The server reads everything, applies the command in pure TypeScript, and sends one &lt;code&gt;client.mutate([...])&lt;/code&gt; transaction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The manifest, every entry and every source are patched with &lt;code&gt;ifRevisionID&lt;/code&gt; set to the &lt;code&gt;_rev&lt;/code&gt; it just read, changed or not. If anything moved in between, Sanity rejects the whole transaction with a 409, and nothing is written.&lt;/li&gt;
&lt;li&gt;New receipts and new editions use &lt;code&gt;create&lt;/code&gt;, never &lt;code&gt;createOrReplace&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Writes use &lt;code&gt;visibility: "sync"&lt;/code&gt;. Success is shown only after a fresh query finds the operation receipt. If a response is lost, the server looks for the receipt before anyone retries; reusing an operation id with different content is rejected by the command hash.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Something I learned from running against the real API.&lt;/strong&gt; In Sanity, any guarded patch gives the document a new &lt;code&gt;_rev&lt;/code&gt;, even if the content is identical. My first version guarded every document by patching it, so each draft edit quietly rewrote the public edition's &lt;code&gt;_rev&lt;/code&gt; and &lt;code&gt;_updatedAt&lt;/code&gt;. The live run caught it; now unchanged editions and receipts are left alone, and an edition's &lt;code&gt;_rev&lt;/code&gt; only moves when it is published again. I verified this against the live dataset: the loom edition kept the same &lt;code&gt;_rev&lt;/code&gt; through later loom and garden writes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verified live.&lt;/strong&gt; I ran an eight-step acceptance checklist against &lt;code&gt;f6kx9jxa&lt;/code&gt;: status, seed (atomic, refuses to overwrite), review and publish, anonymous and &lt;code&gt;/visit&lt;/code&gt; readback, stale translations after an English edit, approvals clearing after a source correction, a history restore that does not republish, a 409 from two browser views and from an independent client racing the app, and withdrawal. The operation ids and &lt;code&gt;_rev&lt;/code&gt; values are in &lt;a href="https://github.com/ToukoUrsin/patchwork/blob/main/docs/VALIDATION.md" rel="noopener noreferrer"&gt;VALIDATION.md&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/ToukoUrsin" rel="noopener noreferrer"&gt;
        ToukoUrsin
      &lt;/a&gt; / &lt;a href="https://github.com/ToukoUrsin/patchwork" rel="noopener noreferrer"&gt;
        patchwork
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Original multilingual editorial accessibility workflow backed by Sanity (DEV Sanity Challenge)
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Patchwork&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Every visitor belongs in the story.&lt;/strong&gt; An editorial workbench for accessible, multilingual museum content, backed by Sanity revisions and inspectable source evidence.&lt;/p&gt;
&lt;p&gt;A museum corrects an entrance description. The English draft changes, but its translated access instructions are still old. Patchwork makes that dependency visible: translations carry the source revision they used, and reviews are bound to exact content and evidence versions. A stale approval cannot publish a new edition.&lt;/p&gt;
&lt;p&gt;Original September 2026 project for &lt;strong&gt;DEV Sanity Challenge, Path Two&lt;/strong&gt;. Substantial AI assistance is disclosed; the app performs no model inference. All museum stories, translations and access measurements are authored synthetic fixtures, not real venue records or professional accessibility assessments.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Live Sanity project&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Project &lt;strong&gt;&lt;code&gt;f6kx9jxa&lt;/code&gt;&lt;/strong&gt;, public dataset &lt;strong&gt;&lt;code&gt;production&lt;/code&gt;&lt;/strong&gt;. Two published trilingual editions, readable without a token:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://f6kx9jxa.api.sanity.io/v2026-09-21/data/query/production?query=*%5B_type%20%3D%3D%20%22patchworkPublication%22%5D" rel="nofollow noopener noreferrer"&gt;https://f6kx9jxa.api.sanity.io/v2026-09-21/data/query/production?query=*%5B_type%20%3D%3D%20%22patchworkPublication%22%5D&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The full live acceptance checklist passed against this project on September 22 PDT, 2026 (&lt;a href="https://github.com/ToukoUrsin/patchwork/docs/VALIDATION.md" rel="noopener noreferrer"&gt;evidence&lt;/a&gt;). Drafts, sources…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/ToukoUrsin/patchwork" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Next.js 16, React 19, TypeScript, &lt;code&gt;@sanity/client&lt;/code&gt;. &lt;code&gt;npm test&lt;/code&gt; runs 20 focused tests (translation lineage, fingerprints, source invalidation, immutable editions, restore, roles, stale versions, idempotency, transaction shape, lost-response reconciliation). MIT licensed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits and disclosure
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Authoring runs on localhost with a server-side token. There is no hosted editor and no real authentication: the editor, reviewer and publisher are clearly labelled &lt;strong&gt;demo roles&lt;/strong&gt;, not independent people.&lt;/li&gt;
&lt;li&gt;The quality gates are presence and wording checks. They do not certify WCAG conformance, reading level or translation accuracy.&lt;/li&gt;
&lt;li&gt;All museum content, translations and measurements are synthetic.&lt;/li&gt;
&lt;li&gt;The optional Studio config is read-only by design and I have not run it against the live project. There are no live listeners; the UI reloads snapshots.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI disclosure:&lt;/strong&gt; built from scratch in September 2026 with substantial help from OpenAI Codex (architecture, code, tests, fixture text, translations, artwork) and Claude (live Sanity verification, the two fixes it found, the film and this write-up). The app itself makes no model calls. The film's narration is an ElevenLabs stock voice; the footage is screen capture of the actual app.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>sanitychallenge</category>
      <category>devchallenge</category>
      <category>nextjs</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
