<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <?xml-stylesheet href="/rss.xsl" type="text/xsl"?>
  <channel>
    <title>srvr.in</title>
    <description>Software · Art · Internet</description>
    <link>https://srvr.in/</link>
    <atom:link href="https://srvr.in/feed.xml" rel="self" type="application/rss+xml"/>
    <pubDate>Fri, 08 May 2026 07:15:15 +0000</pubDate>
    <lastBuildDate>Fri, 08 May 2026 07:15:15 +0000</lastBuildDate>
    <generator>Jekyll v4.4.1</generator>
    
      <item>
        <title>Generative cover images for every post</title>
        <description>&lt;p&gt;Every blog post shared on social media needs an image. Without one, the platform picks something arbitrary — usually nothing good. The standard fix is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;og:image&lt;/code&gt;: a 1200×630 image in the post’s front matter that Twitter, LinkedIn, and iMessage use for the preview card.&lt;/p&gt;

&lt;p&gt;I needed images for thirty-odd posts. The obvious route — Midjourney, Flux, DALL·E — felt wrong for this. AI image generators produce work that looks like AI image generators. Everything comes out with the same aesthetic fingerprint. Worse, the images bear no relationship to the post itself; they are decorative noise.&lt;/p&gt;

&lt;p&gt;What I actually wanted was closer to what Sol LeWitt described as a wall drawing: a rule, applied consistently, that produces something distinct each time. Mathematical art fits this exactly. The image is not chosen or generated by taste — it follows from the input. Same post, same image, always.&lt;/p&gt;

&lt;p&gt;There are several families of patterns that work well at this scale: flow fields (particles traced through a vector field), Truchet tiles (quarter-circle arcs on a grid), string art (straight lines connecting points around circles), Lissajous curves (parametric loops whose shape changes with two integer ratios). Each family is distinct. Each produces images that look designed rather than generated. And crucially, none of them require an API call, a GPU, or a model.&lt;/p&gt;

&lt;h2 id=&quot;how-it-works-on-this-blog&quot;&gt;How it works on this blog&lt;/h2&gt;

&lt;p&gt;A Node.js script takes a post’s slug, hashes it to a seed, and uses that seed to deterministically pick a pattern family, a color palette, and all the parameters within. Same slug, same image, always. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rake np&lt;/code&gt; task that creates new posts now runs the script automatically — new post, new image, no manual step.&lt;/p&gt;

&lt;p&gt;The images live in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assets/images/posts/&lt;/code&gt; as SVGs and are referenced in front matter. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jekyll-seo-tag&lt;/code&gt; picks up the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;image:&lt;/code&gt; field and emits the right OG tags.&lt;/p&gt;

&lt;p&gt;The cover you see at the top of this post is string art — points distributed around two circles, connected by straight lines skipping a fixed interval. The resulting curves look hand-drawn and mathematical at once. Every post on this blog has one, and no two are alike.&lt;/p&gt;
</description>
        <pubDate>Thu, 16 Apr 2026 00:00:00 +0000</pubDate>
        <link>https://srvr.in/software/2026/04/16/generative-cover-images/</link>
        <guid isPermaLink="true">https://srvr.in/software/2026/04/16/generative-cover-images/</guid>
        
        
        <category>software</category>
        
      </item>
    
      <item>
        <title>Omanathinkal Kidavo (ഓമനത്തിങ്കൾക്കിടാവോ) — Malayalam lullaby PDF</title>
        <description>&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;/assets/pdfs/Omanathinkal_Kidavo.pdf&quot;&gt;ഓമനത്തിങ്കൾക്കിടാവോ — PDF (RIT Ezhuthu)&lt;/a&gt;&lt;/strong&gt; · &lt;strong&gt;&lt;a href=&quot;/assets/pdfs/Omanathinkal_Kidavo_Rachana.pdf&quot;&gt;PDF (Rachana)&lt;/a&gt;&lt;/strong&gt; · &lt;em&gt;&lt;a href=&quot;#english&quot;&gt;English version below&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;മലയാളത്തിലെ ഏറ്റവും പ്രിയപ്പെട്ട താരാട്ടുപാട്ടാണ് ‘ഓമനത്തിങ്കൾക്കിടാവോ’. 1813-ൽ തിരുവിതാംകൂർ മഹാറാണി ഗൗരി ലക്ഷ്മി ഭായിയുടെ അഭ്യർത്ഥനപ്രകാരം ഇരയിമ്മൻ തമ്പിയാണ് ഈ വരികൾ രചിച്ചത്. തന്റെ അനന്തരവനായ (ഭാവി മഹാരാജാവ് സ്വാതി തിരുനാൾ രാമവർമ്മ) കുഞ്ഞിന്റെ ജനനം ആഘോഷിക്കാനായിരുന്നു ഇത്. അക്കാലത്ത് ഒരു ആൺവാരിസില്ലാത്തതിനാൽ ബ്രിട്ടീഷുകാർ രാജ്യം പിടിച്ചെടുക്കാൻ (Doctrine of Lapse) സാധ്യതയുണ്ടായിരുന്ന സമയത്ത് നടന്ന ഈ ജനനം രാജകുടുംബത്തിന് വലിയ ആശ്വാസമായിരുന്നു.&lt;/p&gt;

&lt;p&gt;ആ സന്തോഷവും ആശ്വാസവും പാട്ടിന്റെ ഓരോ വരിയിലും പ്രകടമാണ്. കുഞ്ഞിനെ നിലാവിനോടും, പൂവിലെ തേനിനോടും, ആടുന്ന മയിലിനോടും, ദൈവത്തിന്റെ നിധിയോടും, ഭാഗ്യവൃക്ഷത്തിന്റെ ഫലത്തോടുമെല്ലാം കവി ഇതിൽ ഉപമിക്കുന്നു. എന്നാൽ കൗതുകകരമായ ഒരു കാര്യം, പാട്ടിലുടനീളം ഒരിക്കൽ പോലും കുഞ്ഞിനോട് “ഉറങ്ങാൻ” ആവശ്യപ്പെടുന്നില്ല എന്നതാണ്. പാട്ടിന്റെ രാഗത്തിലൂടെയും താളത്തിലൂടെയും കുട്ടി സ്വാഭാവികമായും ഉറങ്ങിക്കൊള്ളും എന്ന സങ്കല്പമാണ് ഇതിന് പിന്നിൽ.&lt;/p&gt;

&lt;p&gt;ആദ്യകാലത്ത് ‘കുറിഞ്ഞി’ രാഗത്തിലും ആദി താളത്തിലുമാണ് ഇത് ചിട്ടപ്പെടുത്തിയത്. എങ്കിലും ഇന്ന് ‘നീലാംബരി’ അല്ലെങ്കിൽ ‘നവരോജ്’ രാഗങ്ങളിലാണ് ഇത് സാധാരണയായി പാടാറുള്ളത്. കെ.എസ്. ചിത്ര ആലപിച്ച രൂപമാണ് ഇന്ന് ഏറ്റവും ജനപ്രിയമായിട്ടുള്ളത്. 1987-ൽ പുറത്തിറങ്ങിയ ‘സ്വാതി തിരുനാൾ’ എന്ന സിനിമയിൽ എസ്. ജാനകിയും ഇതിന്റെ ഒരു ഭാഗം പാടിയിട്ടുണ്ട്.&lt;/p&gt;

&lt;p&gt;2013-ൽ ഈ താരാട്ടുപാട്ട് അന്താരാഷ്ട്ര തലത്തിൽ വാർത്തകളിൽ ഇടം നേടിയിരുന്നു. ‘ലൈഫ് ഓഫ് പൈ’ (Life of Pi) എന്ന ചിത്രത്തിലെ ഓസ്കാർ നാമനിർദ്ദേശം ലഭിച്ച ‘പൈസ് ലല്ലബി’ (Pi’s Lullaby) എന്ന ഗാനം ഈ താരാട്ടുപാട്ടിന്റെ തമിഴ് വിവർത്തനമാണെന്ന് ആരോപിച്ച് ഇരയിമ്മൻ തമ്പി മെമ്മോറിയൽ ട്രസ്റ്റ് രംഗത്തെത്തിയിരുന്നു. എന്നാൽ ബോംബെ ജയശ്രീ ഈ ആരോപണം നിഷേധിച്ചു.&lt;/p&gt;

&lt;h2 id=&quot;പിഡിഎഫ്&quot;&gt;പിഡിഎഫ്&lt;/h2&gt;

&lt;p&gt;ഈ പാട്ടിലെ 25 ഈരടികളും പൂർണ്ണമായി ഉൾക്കൊള്ളുന്ന, ഭംഗിയുള്ളതും പ്രിന്റ് ചെയ്യാവുന്നതുമായ ഒരു എ4 ഷീറ്റ് തയ്യാറാക്കണമെന്ന് എനിക്കുണ്ടായിരുന്നു. ഇത് ഫ്രെയിം ചെയ്ത് വെക്കാനോ കുഞ്ഞുങ്ങളുടെ തൊട്ടിലരികിൽ സൂക്ഷിക്കാനോ അനുയോജ്യമാണ്.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ഡൗൺലോഡ്: &lt;a href=&quot;/assets/pdfs/Omanathinkal_Kidavo.pdf&quot;&gt;ഓമനത്തിങ്കൾക്കിടാവോ — RIT Ezhuthu (PDF)&lt;/a&gt;&lt;/strong&gt; · &lt;strong&gt;&lt;a href=&quot;/assets/pdfs/Omanathinkal_Kidavo_Rachana.pdf&quot;&gt;Rachana (PDF)&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&quot;മലയാളം-ലിപി-പിഡിഎഫിൽ-കൃത്യമാക്കിയ-വിധം&quot;&gt;മലയാളം ലിപി പിഡിഎഫിൽ കൃത്യമാക്കിയ വിധം&lt;/h2&gt;

&lt;p&gt;മലയാളം ലിപി ഒരു പിഡിഎഫിൽ കൃത്യമായി തെളിയുക എന്നത് വിചാരിച്ചതിലും പ്രയാസകരമായ ഒന്നായിരുന്നു.&lt;/p&gt;

&lt;p&gt;മലയാളം വളരെ സങ്കീർണ്ണമായ ഒരു ലിപിയാണ്. ചില്ലക്ഷരങ്ങൾ (ൻ, ൽ, ൾ, ർ, ൺ), കൂട്ടക്ഷരങ്ങൾ എന്നിവ അവയുടെ സ്ഥാനത്തിനനുസരിച്ച് രൂപം മാറുന്നവയാണ്. ഇതിന് ‘ഓപ്പൺ ടൈപ്പ് ടെക്സ്റ്റ് ഷേപ്പിംഗ്’ (OpenType text shaping) ആവശ്യമാണ്. അതായത്, ഫോണ്ടിലെ വിവരങ്ങൾ (GSUB, GPOS) വിശകലനം ചെയ്ത് അക്ഷരങ്ങളെ കൃത്യമായി യോജിപ്പിക്കേണ്ടതുണ്ട്. ‘ReportLab’ പോലുള്ള ലൈബ്രറികൾ ഉപയോഗിച്ച് വെറുതെ യൂണിക്കോഡ് കോഡുകൾ നൽകിയാൽ അക്ഷരങ്ങൾ തെറ്റായിട്ടായിരിക്കും വരിക.&lt;/p&gt;

&lt;p&gt;ഇതിനായി ഞാൻ സ്വീകരിച്ച വഴി ഇതാണ്:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;വരികൾ ആദ്യം ഒരു HTML പേജ് ആയി തയ്യാറാക്കി.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://rachana.org.in&quot;&gt;&lt;strong&gt;റചന (Rachana) ടൈപ്പോഗ്രഫി&lt;/strong&gt;&lt;/a&gt; നിർമ്മിച്ച &lt;a href=&quot;https://rachana.org.in/downloads.html#Ezhuthu&quot;&gt;&lt;strong&gt;എഴുത്ത് (RIT Ezhuthu)&lt;/strong&gt;&lt;/a&gt; ഫോണ്ട് ഉപയോഗിച്ചു. ഇതിൽ മലയാളം അക്ഷരങ്ങളുടെ എല്ലാ നിയമങ്ങളും കൃത്യമായി ഉൾക്കൊള്ളിച്ചിട്ടുണ്ട്.&lt;/li&gt;
  &lt;li&gt;ഇതിനെ &lt;strong&gt;Chrome headless&lt;/strong&gt; ഉപയോഗിച്ച് പിഡിഎഫ് ആക്കി മാറ്റി. ഇതിലെ HarfBuzz എന്ന ഷേപ്പിംഗ് എഞ്ചിൻ (ഇത് തന്നെയാണ് ഫയർഫോക്സ്, ക്രോം, ആൻഡ്രോയിഡ് എന്നിവയിലും ഉപയോഗിക്കുന്നത്) മലയാളം അക്ഷരങ്ങളെ കൃത്യമായി രൂപപ്പെടുത്തുന്നു.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;ഇതിന്റെ ഫലമായി 25 ഈരടികളും അടങ്ങുന്ന, വായിക്കാൻ സുഖമുള്ളതും പ്രിന്റ് ചെയ്യാവുന്നതുമായ ഒരു പേജ് എനിക്ക് ലഭിച്ചു.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;em&gt;&lt;a href=&quot;#english&quot;&gt;English version below&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;a name=&quot;english&quot;&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Omanathinkal Kidavo is perhaps the most beloved lullaby in Malayalam. It was composed in 1813 by the court poet Irayimman Thampi, at the request of Maharani Gowri Lakshmi Bayi of Travancore, to celebrate the birth of her nephew — the future Maharajah Swathi Thirunal Rama Varma. The birth was a momentous event for the royal family, which had been facing the threat of annexation by the British under the Doctrine of Lapse for want of a male heir.&lt;/p&gt;

&lt;p&gt;That relief and joy runs through every line. The baby is compared to moonlight, honey in a flower, a dancing peacock, a treasure from God, the fruit of the tree of fortune — but never once asked to sleep. The sleep is meant to come from the raga itself.&lt;/p&gt;

&lt;p&gt;Originally set in Kurinji raga and Adi tala, it is most commonly performed today in the Nilambari or Navaroj ragas. K.S. Chithra’s rendition is the most widely known. S. Janaki sang a portion of it in the 1987 biographical film &lt;em&gt;Swathi Thirunal&lt;/em&gt;. The poem made international headlines in 2013 when the Irayimman Thampi Memorial Trust alleged that the first eight lines of Bombay Jayashri’s Oscar-nominated “Pi’s Lullaby” from &lt;em&gt;Life of Pi&lt;/em&gt; were a Tamil translation of this lullaby — a claim Jayashri denied.&lt;/p&gt;

&lt;h2 id=&quot;the-pdf&quot;&gt;The PDF&lt;/h2&gt;

&lt;p&gt;I wanted a clean, printable A4 sheet with all 25 couplets in Malayalam — something you could frame or keep by a crib.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;/assets/pdfs/Omanathinkal_Kidavo.pdf&quot;&gt;Download: Omanathinkal Kidavo — RIT Ezhuthu (PDF)&lt;/a&gt;&lt;/strong&gt; · &lt;strong&gt;&lt;a href=&quot;/assets/pdfs/Omanathinkal_Kidavo_Rachana.pdf&quot;&gt;Rachana (PDF)&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&quot;getting-malayalam-right-in-a-pdf&quot;&gt;Getting Malayalam right in a PDF&lt;/h2&gt;

&lt;p&gt;Getting Malayalam to render correctly in a PDF turned out to be more involved than expected.&lt;/p&gt;

&lt;p&gt;Malayalam is a complex script. Characters like chillaksharams (ൻ, ൽ, ൾ, ർ, ൺ) and conjunct consonants change shape depending on context. They require OpenType text shaping: the rendering engine consults lookup tables in the font (GSUB and GPOS) to substitute and reposition glyphs. A naive approach — placing Unicode codepoints onto a PDF canvas as libraries like ReportLab do with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;drawString&lt;/code&gt; — skips this shaping step entirely, producing broken ligatures and garbled chillaksharams.&lt;/p&gt;

&lt;p&gt;The solution was a proper shaping pipeline:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Write the lyrics as an HTML page with CSS for layout, borders, and typography.&lt;/li&gt;
  &lt;li&gt;Use the &lt;a href=&quot;https://rachana.org.in/downloads.html#Ezhuthu&quot;&gt;&lt;strong&gt;RIT Ezhuthu&lt;/strong&gt;&lt;/a&gt; font from &lt;a href=&quot;https://rachana.org.in&quot;&gt;Rachana Typography&lt;/a&gt; — a well-crafted OpenType font with complete Malayalam shaping rules.&lt;/li&gt;
  &lt;li&gt;Convert to PDF using &lt;strong&gt;Chrome headless&lt;/strong&gt;, which uses &lt;strong&gt;HarfBuzz&lt;/strong&gt; — the same shaping engine used by Firefox, Chrome, and Android — to process the OpenType tables and produce properly formed Malayalam glyphs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result is a single A4 page with all 25 couplets, readable and print-ready.&lt;/p&gt;
</description>
        <pubDate>Wed, 15 Apr 2026 00:00:00 +0000</pubDate>
        <link>https://srvr.in/malayalam/2026/04/15/omanathinkal-kidavo-malayalam-lullaby-pdf/</link>
        <guid isPermaLink="true">https://srvr.in/malayalam/2026/04/15/omanathinkal-kidavo-malayalam-lullaby-pdf/</guid>
        
        
        <category>malayalam</category>
        
      </item>
    
      <item>
        <title>A simple i18n strategy for single-page apps</title>
        <description>&lt;p&gt;I built &lt;a href=&quot;https://nokkam.lol/&quot;&gt;nokkam&lt;/a&gt; — a Kerala election prediction game — and a week before the April 9 elections, I wanted to add Malayalam. I looked at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;react-i18next&lt;/code&gt; and FormatJS. Both are built for apps that need plural rules, date formatting, server-side loading, and right-to-left support. Nokkam has none of that. It is a two-language SPA with static strings and no backend.&lt;/p&gt;

&lt;p&gt;I did not want to wire up a plugin system for a problem that did not need one. So I wrote the i18n layer myself. Here is what I needed:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Two languages only&lt;/li&gt;
  &lt;li&gt;Type safety: if &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ml.ts&lt;/code&gt; is missing a key that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;en.ts&lt;/code&gt; has, fail at compile time, not runtime&lt;/li&gt;
  &lt;li&gt;Language preference persists across page reload&lt;/li&gt;
  &lt;li&gt;No build step, no code generation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is it. The result is 60 lines.&lt;/p&gt;

&lt;h2 id=&quot;the-type-trick&quot;&gt;The type trick&lt;/h2&gt;

&lt;p&gt;The English file is a plain object. The last line is the important one:&lt;/p&gt;

&lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// src/locales/en.ts&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;en&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;splash&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Kerala Assembly Election&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Prediction Game&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;tap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Tap to begin&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;leaderboard&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;predictionSingular&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;prediction&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;predictionPlural&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;predictions&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;constituencies&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Manjeshwara&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Kasaragod&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ... 140 entries&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;as &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;Record&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;districts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;Kasaragodu&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Kasaragodu&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ... 14 entries&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;as &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;Record&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Translations&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;typeof&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;en&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;typeof en&lt;/code&gt; generates a type from the object’s shape. The Malayalam file imports that type and must satisfy it:&lt;/p&gt;

&lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// src/locales/ml.ts&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Translations&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;./en&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ml&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Translations&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;splash&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;കേരള നിയമസഭാ തിരഞ്ഞെടുപ്പ്&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;പ്രവചന മത്സരം&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;tap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;തുടങ്ങാൻ ടാപ്പ് ചെയ്യുക&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;leaderboard&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;predictionSingular&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;പ്രവചനം&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;predictionPlural&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;പ്രവചനങ്ങൾ&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If I add a key to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;en.ts&lt;/code&gt; and forget &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ml.ts&lt;/code&gt;, TypeScript throws a compile error. No runtime surprises, no JSON schema files, no code generation step.&lt;/p&gt;

&lt;p&gt;One thing to avoid: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;as const&lt;/code&gt; on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;en&lt;/code&gt; object. That makes every string a literal type, so &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ml.ts&lt;/code&gt; cannot satisfy &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Translations&lt;/code&gt; — every Malayalam string would need to match the English one character for character.&lt;/p&gt;

&lt;h2 id=&quot;the-context&quot;&gt;The context&lt;/h2&gt;

&lt;p&gt;The provider reads the saved preference from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;localStorage&lt;/code&gt; on mount, then exposes the current translations and a toggle function:&lt;/p&gt;

&lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// src/lib/i18n.tsx&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;createContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ReactNode&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;react&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;en&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;../locales/en&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ml&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;../locales/ml&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Translations&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;../locales/en&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Lang&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;en&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;ml&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;LOCALES&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Record&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Lang&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Translations&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;en&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ml&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;STORAGE_KEY&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;nokkam_lang&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kr&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;I18nCtx&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;tr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Translations&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;lang&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Lang&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;toggleLang&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;tC&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;num&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;tD&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Ctx&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;createContext&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;I18nCtx&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;I18nProvider&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;children&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;children&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ReactNode&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;lang&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;setLang&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Lang&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;stored&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;localStorage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getItem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;STORAGE_KEY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;stored&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;ml&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;ml&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;en&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tr&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;LOCALES&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;lang&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;toggleLang&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Lang&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;lang&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;en&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;ml&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;en&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;localStorage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;setItem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;STORAGE_KEY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;setLang&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;tC&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;num&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;constituencies&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;num&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;en&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;constituencies&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;num&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;num&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;tD&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;districts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;return &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Ctx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Provider&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;lang&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;toggleLang&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tC&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tD&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}}&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;children&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/Ctx.Provider&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;gt;
&lt;/span&gt;  &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;useTranslation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;I18nCtx&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ctx&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;useContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Ctx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;useTranslation used outside I18nProvider&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;A component calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useTranslation()&lt;/code&gt; and reads from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tr&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-tsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// src/screens/SplashScreen.tsx&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;SplashScreen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;onNext&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Props&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tr&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;useTranslation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;line1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;line2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;splash&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;splash-screen&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;onClick&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;onNext&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;h1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;line1&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;br&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;/&amp;gt;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;line2&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;h1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;splash&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tap&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;div&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The toggle button shows “മ” in English mode and “EN” in Malayalam:&lt;/p&gt;

&lt;div class=&quot;language-tsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// src/components/LangToggle.tsx&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;LangToggle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;lang&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;toggleLang&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;useTranslation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;onClick&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toggleLang&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;aria-label&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Toggle language&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;lang&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;en&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;മ&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;EN&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;button&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;the-proper-nouns-problem&quot;&gt;The proper nouns problem&lt;/h2&gt;

&lt;p&gt;Kerala has 140 assembly constituencies and 14 districts. These are proper nouns — each has a canonical Malayalam spelling that differs from transliteration, and they are looked up by ID, not by a developer-chosen key.&lt;/p&gt;

&lt;p&gt;i18n libraries handle this badly. They assume every string has a developer-chosen key.&lt;/p&gt;

&lt;p&gt;The solution here is two separate maps inside the locale file, typed with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Record&amp;lt;number, string&amp;gt;&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Record&amp;lt;string, string&amp;gt;&lt;/code&gt;. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tC(num)&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tD(name)&lt;/code&gt; helpers look up those maps and fall back to English if the key is missing — safe for incremental rollout.&lt;/p&gt;

&lt;h2 id=&quot;the-translation-workflow&quot;&gt;The translation workflow&lt;/h2&gt;

&lt;p&gt;I wrote the first pass myself, with Claude helping inline. Then I embedded a review prompt as a block comment at the bottom of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ml.ts&lt;/code&gt; and pasted the whole file into a separate chat.&lt;/p&gt;

&lt;p&gt;The corrections that came back:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;My version&lt;/th&gt;
      &lt;th&gt;Correct&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;കാസർഗോഡ്&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;കാസർകോട്&lt;/code&gt; (official district name)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ആടൂർ&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;അടൂർ&lt;/code&gt; (constituency #115)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;അറ്റിങ്ങൽ&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ആറ്റിങ്ങൽ&lt;/code&gt; (Attingal)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;അരൻമുള&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ആറന്മുള&lt;/code&gt; (Aranmula)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;നടപ്പുറം&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;നാദാപുരം&lt;/code&gt; (Nadapuram)&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The review also made several UI strings more colloquial.&lt;/p&gt;

&lt;p&gt;The workflow is reproducible: write the review prompt once, embed it in the source, paste the file whenever you want a pass. It works for any language.&lt;/p&gt;

&lt;h2 id=&quot;where-this-breaks-down&quot;&gt;Where this breaks down&lt;/h2&gt;

&lt;p&gt;More than two or three languages, and the object approach stops scaling. Malayalam has complex plural forms — the current setup handles it with separate keys (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;predictionSingular&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;predictionPlural&lt;/code&gt;), which works but is not general. A proper i18n library handles pluralisation with a rules engine. Server-rendered apps need a different loading strategy. For ICU message format, date and number localisation, or RTL support: use a library.&lt;/p&gt;

&lt;p&gt;For a small SPA with a fixed set of languages and no pluralisation complexity, this is enough.&lt;/p&gt;
</description>
        <pubDate>Mon, 06 Apr 2026 00:00:00 +0000</pubDate>
        <link>https://srvr.in/software/2026/04/06/spa-i18n-malayalam/</link>
        <guid isPermaLink="true">https://srvr.in/software/2026/04/06/spa-i18n-malayalam/</guid>
        
        
        <category>software</category>
        
      </item>
    
      <item>
        <title>Whisper dictation on Linux</title>
        <description>&lt;p&gt;macOS has built-in dictation. Windows has it too. Linux does not — not in any form that works system-wide, without the cloud, and without a painful setup ritual. So I built one.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/srih4ri/whisper-dictation&quot;&gt;whisper-dictation&lt;/a&gt; is a single Python file (~300 lines). Hold a keyboard shortcut, speak, release — your words appear in whatever app has focus. It runs entirely locally using OpenAI’s Whisper model via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;faster-whisper&lt;/code&gt;. No API keys. No cloud. No subscriptions.&lt;/p&gt;

&lt;p&gt;The interesting part was not the speech recognition. That was easy. The interesting part was getting the transcribed text &lt;em&gt;into&lt;/em&gt; the active window without breaking things.&lt;/p&gt;

&lt;h2 id=&quot;the-stack&quot;&gt;The stack&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;faster-whisper&lt;/strong&gt; — CTranslate2-optimized Whisper with int8 quantization. The “base” model is 140MB and transcribes a few seconds of audio in about a second on a laptop CPU.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;sounddevice&lt;/strong&gt; — PortAudio wrapper for mic capture.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;evdev&lt;/strong&gt; — reads keyboard events directly from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/dev/input/*&lt;/code&gt;. On Wayland, there is no equivalent of X11’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;XGrabKey&lt;/code&gt;, so reading from the input device directly is the only way to get global hotkeys.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;uv&lt;/strong&gt; — the script uses &lt;a href=&quot;https://peps.python.org/pep-0723/&quot;&gt;PEP 723 inline metadata&lt;/a&gt;, so &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv run ./dictation.py&lt;/code&gt; installs everything automatically. No venv to manage.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;the-space-eating-bug&quot;&gt;The space-eating bug&lt;/h2&gt;

&lt;p&gt;The first version worked: evdev catches the hotkey, sounddevice records while it is held, faster-whisper transcribes on release, ydotool types the result. Had it running in an afternoon.&lt;/p&gt;

&lt;p&gt;Then I tried using it with Claude Code.&lt;/p&gt;

&lt;p&gt;Every space disappeared. The transcription in the logs was perfect — “Hello, I am testing this dictation tool” — but what appeared in the terminal was “Hello,Iamtestingthisdictationtool”.&lt;/p&gt;

&lt;p&gt;The cause: Claude Code uses the space bar as a shortcut to activate voice chat mode. When &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ydotool type&lt;/code&gt; sends text character by character, each space is a separate key event. Claude Code intercepts those events before they reach the input field.&lt;/p&gt;

&lt;p&gt;This is the kind of bug that only shows up when the tool meets a specific app’s keyboard handling. Unit tests would never catch it.&lt;/p&gt;

&lt;p&gt;The fix is to not type characters at all. Instead:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Copy the transcribed text to the clipboard (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wl-copy&lt;/code&gt; on Wayland)&lt;/li&gt;
  &lt;li&gt;Simulate Ctrl+Shift+V to paste it as a block&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Simple in theory. In practice, this was a tour of everything broken about input simulation on Linux Wayland.&lt;/p&gt;

&lt;h2 id=&quot;the-wayland-input-simulation-gauntlet&quot;&gt;The Wayland input simulation gauntlet&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Attempt 1: wl-copy + ydotool key&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;First try: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wl-copy &quot;text&quot;&lt;/code&gt; to set clipboard, then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ydotool key ctrl+shift+v&lt;/code&gt; to paste. The script hung. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wl-copy&lt;/code&gt; does not fork on newer versions — it stays running as a clipboard server process, serving paste requests. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;subprocess.run()&lt;/code&gt; blocks forever waiting for it to exit.&lt;/p&gt;

&lt;p&gt;Fix: use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;subprocess.Popen()&lt;/code&gt; and kill the previous instance before each new copy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 2: ydotool syntax mismatch&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The system had ydotool v0.1.8 (Ubuntu’s default), and I was using v1.0+ raw keycode syntax (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;29:1 42:1 47:1...&lt;/code&gt;). v0.1.8 uses named syntax (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ctrl+shift+v&lt;/code&gt;). Different tool, different interface.&lt;/p&gt;

&lt;p&gt;Also: the setup script had created a systemd service for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ydotoold&lt;/code&gt;, the ydotool daemon. That binary does not exist in v0.1.8 — it was added in v1.0+. The service crash-looped with exit code 203 (EXEC: binary not found). Without the daemon, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ydotool key&lt;/code&gt; was unreliable for single key combos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 3: wtype&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wtype&lt;/code&gt; is the Wayland-native text input tool. It uses the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wlr-virtual-keyboard-v1&lt;/code&gt; protocol.&lt;/p&gt;

&lt;p&gt;Exit code 1. GNOME/Mutter does not implement &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wlr-virtual-keyboard-v1&lt;/code&gt;. That protocol is specific to wlroots-based compositors (Sway, Hyprland). wtype is useless on GNOME.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 4: xdotool&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;xdotool key ctrl+shift+v&lt;/code&gt;. Nothing happened. My terminal (Ptyxis, GNOME’s new GTK4 terminal) is a native Wayland app. xdotool only reaches XWayland windows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually works: ydotool with a delay&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Back to ydotool. The key insight: ydotool operates at the kernel level via uinput. It creates a virtual input device that the Wayland compositor treats as a real keyboard. This works with every window — Wayland-native, XWayland, GTK, Qt, terminal, browser.&lt;/p&gt;

&lt;p&gt;The trick for reliability without the daemon: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ydotool key --delay 200 ctrl+shift+v&lt;/code&gt;. The 200ms delay gives the compositor time to register the virtual keyboard device before the key events arrive.&lt;/p&gt;

&lt;p&gt;Final working pipeline on GNOME Wayland:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;wl-copy &quot;text&quot;  →  sleep 100ms  →  ydotool key --delay 200 ctrl+shift+v
(Popen, non-blocking)              (uinput → compositor → active window)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The compatibility breakdown across tools:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Tool&lt;/th&gt;
      &lt;th&gt;Mechanism&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;GNOME Wayland&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Sway/Hyprland&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;X11&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;ydotool&lt;/td&gt;
      &lt;td&gt;Linux uinput (kernel)&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Yes&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Yes&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Yes&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;wtype&lt;/td&gt;
      &lt;td&gt;wlr-virtual-keyboard-v1&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;No&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Yes&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;No&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;xdotool&lt;/td&gt;
      &lt;td&gt;X11 protocol&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;No*&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;No&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Yes&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;*Only reaches XWayland windows&lt;/p&gt;

&lt;h2 id=&quot;a-few-other-decisions&quot;&gt;A few other decisions&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Hotkey combo, not single key.&lt;/strong&gt; The initial hotkey was just Right Super (KEY_RIGHTMETA). On GNOME, releasing Super opens the Activities overview. Every dictation ended with the app launcher flashing open. Switched to Right Shift + Right Super as the default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool detection at startup.&lt;/strong&gt; All the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shutil.which()&lt;/code&gt; checks for wl-copy, ydotool, xclip, xdotool run once when the script boots. During actual use, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;type_text()&lt;/code&gt; just branches on a cached string — no overhead per transcription.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Warnings, not crashes.&lt;/strong&gt; If the ideal tools are not installed, the script tells you exactly what to install, but still tries to work with whatever is available. It falls back to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ydotool type&lt;/code&gt; if nothing else is found — which works everywhere except in apps with custom space-bar handling.&lt;/p&gt;

&lt;h2 id=&quot;what-i-learned&quot;&gt;What I learned&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;wl-copy is a long-lived process.&lt;/strong&gt; It does not fire and forget. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;subprocess.run()&lt;/code&gt; will block your event loop forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ydotool 0.1.8 and ydotool 1.0+ are different tools.&lt;/strong&gt; Different syntax, different architecture (daemon vs. direct), different reliability characteristics. Ubuntu ships the old one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Clipboard paste is more robust than typing.&lt;/strong&gt; Any app that does custom key handling — vim, tmux, Claude Code — may interpret individual key events differently than raw text. Pasting as a block sidesteps all of that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Debug logging saves hours.&lt;/strong&gt; Adding &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--debug&lt;/code&gt; early and logging every subprocess call with its exit code and stderr made each failure diagnosable in one iteration instead of three.&lt;/p&gt;

&lt;h2 id=&quot;try-it&quot;&gt;Try it&lt;/h2&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git clone https://github.com/srih4ri/whisper-dictation
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;whisper-dictation
./setup.sh
&lt;span class=&quot;c&quot;&gt;# log out, log back in&lt;/span&gt;
./dictation.py
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Tested on Ubuntu 25.10 with GNOME 48/Mutter and Ptyxis terminal. The setup script detects your display server and installs the right tools. If something does not work on your setup, open an issue.&lt;/p&gt;
</description>
        <pubDate>Mon, 30 Mar 2026 00:00:00 +0000</pubDate>
        <link>https://srvr.in/software/2026/03/30/whisper-dictation/</link>
        <guid isPermaLink="true">https://srvr.in/software/2026/03/30/whisper-dictation/</guid>
        
        
        <category>software</category>
        
      </item>
    
      <item>
        <title>llm-relay: switching LLM backends with one env var</title>
        <description>&lt;p&gt;I have a homelab service that classifies incoming text using an LLM. I wanted different models in different environments: &lt;a href=&quot;/software/2026/03/18/apple-intelligence-on-the-command-line/&quot;&gt;Apple Intelligence&lt;/a&gt; on my Mac (free, private), a quantised DeepSeek-R1 on Ollama on my home server, and AWS Bedrock on my cloud VPS. Each has a different SDK, a different async interface, and different configuration.&lt;/p&gt;

&lt;p&gt;I did not want to write &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;if backend == &quot;anthropic&quot;&lt;/code&gt; branches inside application code. So I extracted the problem into a small package: &lt;a href=&quot;https://github.com/srih4ri/llm-relay&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;llm-relay&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The package is small by design. It is not a framework. It does not manage conversation history, streaming, or embeddings. It just routes a prompt to a model and returns a string — and that constraint is what keeps it useful across multiple unrelated services.&lt;/p&gt;

&lt;h2 id=&quot;what-it-does&quot;&gt;What it does&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;llm-relay&lt;/code&gt; exposes two functions:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;llm_relay&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ask&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ask_json&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;ask&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;summarise this&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;system&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;You are a summariser&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;ask_json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;classify: buy milk&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# → {&quot;category&quot;: &quot;shopping&quot;, &quot;title&quot;: &quot;Buy milk&quot;, ...}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The backend is controlled entirely by environment variables:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;LLM_BACKEND=ollama       # or anthropic, bedrock, apple
LLM_MODEL=deepseek-r1:1.5b
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Application code never changes. Only the deployment environment does.&lt;/p&gt;

&lt;h2 id=&quot;how-it-works&quot;&gt;How it works&lt;/h2&gt;

&lt;p&gt;It is mostly &lt;a href=&quot;https://github.com/BerriAI/litellm&quot;&gt;LiteLLM&lt;/a&gt; with Apple Intelligence support bolted on. For Ollama, Anthropic, and Bedrock it delegates to LiteLLM, which handles the protocol differences between providers. For Apple Intelligence it calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;apple_fm_sdk&lt;/code&gt; directly, since the on-device Foundation Models framework has its own async interface that LiteLLM does not cover (see &lt;a href=&quot;/software/2026/03/18/apple-intelligence-on-the-command-line/&quot;&gt;Apple Intelligence on the command line&lt;/a&gt;).&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;ask&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;prompt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;system&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;cfg&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;config&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;or&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LLMConfig&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;from_env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cfg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;backend&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;apple&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;_ask_apple&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;prompt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;system&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;system&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cfg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cfg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;_ask_litellm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;prompt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;system&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;system&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cfg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cfg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ask_json&lt;/code&gt; is a thin wrapper that also strips markdown fences, since models habitually wrap JSON in ` ```json ` blocks even when asked not to.&lt;/p&gt;

&lt;h2 id=&quot;how-it-is-installed&quot;&gt;How it is installed&lt;/h2&gt;

&lt;p&gt;A plain Python package published to GitHub, installed as a git dependency:&lt;/p&gt;

&lt;div class=&quot;language-toml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# pyproject.toml&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;dependencies&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&quot;llm-relay @ git+https://github.com/srih4ri/llm-relay.git@v0.1.0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
</description>
        <pubDate>Fri, 20 Mar 2026 00:00:00 +0000</pubDate>
        <link>https://srvr.in/software/2026/03/20/llm-relay-switching-llm-backends-with-one-env-var/</link>
        <guid isPermaLink="true">https://srvr.in/software/2026/03/20/llm-relay-switching-llm-backends-with-one-env-var/</guid>
        
        
        <category>software</category>
        
      </item>
    
      <item>
        <title>Rise of the machines - watching the AI big bang unravel</title>
        <description>&lt;p&gt;I lived through COVID without writing a word about it. The spread, the panic, the lockdowns, the strange rituals — masks, hand sanitizer at every door, the six-foot distance that became reflex. Then the vaccines, and the slow return to something ordinary. And then one day, a year or two later, I realized: I had no record of having been there. Something historical had happened around me. I had nothing to show for having witnessed it.&lt;/p&gt;

&lt;p&gt;I do not want to make that mistake again.&lt;/p&gt;

&lt;p&gt;Something large is happening right now with AI. I can take notes while it happens. That is what this post is.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-timeline&quot;&gt;The timeline&lt;/h2&gt;

&lt;h3 id=&quot;2022--first-words-with-chatgpt&quot;&gt;~2022 — First words with ChatGPT&lt;/h3&gt;

&lt;p&gt;I do not remember the exact date. I was poking at it — asking questions to find where it breaks, watching it give stupid replies, laughing. &lt;em&gt;This is dumb.&lt;/em&gt; That was my conclusion. I closed the tab.&lt;/p&gt;

&lt;h3 id=&quot;20222023--a-blurry-image-from-a-machine&quot;&gt;~2022–2023 — A blurry image from a machine&lt;/h3&gt;

&lt;p&gt;I opened a Stable Diffusion web UI and typed a prompt. What came out was laughable. It did not draw what I asked. Faces melted together. Hands had seven fingers.&lt;/p&gt;

&lt;p&gt;Fast forward two years: text-to-image works almost flawlessly. No ghost fingers. No obvious artifacts. You can still tell — there is a quality to AI images that gives them away. But there are no blatant mistakes. No unnatural features. The gap between what you ask for and what you get has nearly closed.&lt;/p&gt;

&lt;h3 id=&quot;early-2024--annoyance-and-a-policy&quot;&gt;Early 2024 — Annoyance, and a policy&lt;/h3&gt;

&lt;p&gt;People started sending AI-generated pull requests. The code was not bad in obvious ways — it was formatted, described, plausible. But it made mistakes. Subtle ones. The kind a careful reviewer catches and a careless one ships. I was annoyed that people were not being careful, that they were throwing AI output at me to review as if I were a human compiler.&lt;/p&gt;

&lt;p&gt;My own policy at the time: get AI to write a function — something small, with a clearly defined interface, where you can judge whether it works. But compose those functions yourself. Orchestrate yourself. Keep the reasoning yours.&lt;/p&gt;

&lt;p&gt;I thought this was the paradigm for a while. Write the small pieces with AI. Own the structure. I did not think AI would ever reason about a large codebase and produce working code from a bare requirement. I was wrong, but I did not know that yet.&lt;/p&gt;

&lt;h3 id=&quot;early-2024--copy-paste-distrust&quot;&gt;Early 2024 — Copy, paste, distrust&lt;/h3&gt;

&lt;p&gt;At work, I was not using any coding agent. The company had not completed the legal process to allow AI tools access to the codebase — privacy concerns, data exposure. I had no access.&lt;/p&gt;

&lt;p&gt;Outside of that, I was using ChatGPT for coding the only way available: copy a snippet, paste a question, paste the answer back. It worked sometimes. But I never fully trusted the output. The code looked right. It often was not. I had no way to verify it quickly, and that uncertainty made me hesitant to lean on it. I did not know what coding agents existed or how to evaluate them. ChatGPT and the occasional Google search were the whole toolkit.&lt;/p&gt;

&lt;h3 id=&quot;august-2024--agentic-coding-first-attempt&quot;&gt;August 2024 — Agentic coding, first attempt&lt;/h3&gt;

&lt;p&gt;I was using GitHub Copilot from inside VS Code — agentic mode, letting it do more than autocomplete. Getting it to actually make a code change required nagging.&lt;/p&gt;

&lt;h3 id=&quot;june-2025--laid-off-and-leaning-in&quot;&gt;June 2025 — Laid off, and leaning in&lt;/h3&gt;

&lt;p&gt;I was laid off. I used AI more heavily than I ever had before. I exported my entire work history from Jira and fed it to AI to build my resume — not to write it for me, but to surface what mattered from years of tickets and notes. The result is &lt;a href=&quot;https://srvr.in/resume/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The original two-page resume was not working. I was applying and hearing nothing. We figured out that AI was probably doing the first-pass screening and auto-rejecting me. So I made the resume bigger. I also built workflows to generate cover letters from job descriptions and my own resume. It felt strange to do. Mechanical in a way I had not expected.&lt;/p&gt;

&lt;p&gt;And then the eerie part: I was being rejected by AI when applying for jobs. I kept thinking — people are now going to encounter AI as their first point of contact with companies, and the AI may give them nothing useful.&lt;/p&gt;

&lt;p&gt;It did not take long. Within months, AI chatbots were everywhere. Most of them are nearly useless. They send you in circles. They deflect. They cannot handle anything outside the script. Maybe they reduce call volume for companies, but as a customer — trying to cancel an airline ticket, trying to do something that requires actual judgment — the chatbot does not help. It gets in the way.&lt;/p&gt;

&lt;h3 id=&quot;november-2025--claude&quot;&gt;November 2025 — Claude&lt;/h3&gt;

&lt;p&gt;I switched to Claude. What struck me immediately was how it used tools — and how quickly. With Copilot, or Codex before it, I had to push the agent to make a change. Claude just did it. You asked, it reached for the file, made the change, moved on.&lt;/p&gt;

&lt;p&gt;Around the same time, I tried to stop Claude from co-signing my commits. I fiddled with it for a bit, then stopped caring, and Claude has co-signed everything since. Someone told me they actually liked it — it shows they used Claude, not Copilot.&lt;/p&gt;

&lt;h3 id=&quot;march-18-2026--ten-minutes&quot;&gt;March 18, 2026 — Ten minutes&lt;/h3&gt;

&lt;p&gt;I ran Claude Code with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--dangerously-skip-permissions&lt;/code&gt;. Along with the task, I told it: find the Telegram bot credentials somewhere in this repo, find my Telegram contact from any message history, and send me a message when you are done. I thought the task would take hours. I expected it might surprise me by finishing sooner. I did not expect it to finish the task, hunt down the credentials, find my contact, and actually send me a Telegram message — all in under ten minutes.&lt;/p&gt;

&lt;h3 id=&quot;march-2026--the-first-agent-at-work&quot;&gt;March 2026 — The first agent at work&lt;/h3&gt;

&lt;p&gt;I wrote my first agentic program at work: a system that reads incoming alerts and triages them autonomously. It reads context, makes a judgment, acts. It does something that used to require a human on-call at 3am.&lt;/p&gt;

&lt;p&gt;I built it in a week.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;april-2026--nokkam-quota-and-talking-to-claude&quot;&gt;April 2026 — Nokkam, quota, and talking to Claude&lt;/h3&gt;

&lt;p&gt;Last week I built &lt;a href=&quot;https://nokkam.lol&quot;&gt;nokkam.lol&lt;/a&gt; — a Kerala Assembly Elections 2026 prediction game where users pick winners across all 140 constituencies, get a shareable card, and land on a leaderboard. The frontend is React + TypeScript on Cloudflare Pages; the backend is Go + SQLite on a VPS. The app ships with full Malayalam i18n, anonymous user IDs, and scoring logic that sits dormant until results day on May 4.&lt;/p&gt;

&lt;p&gt;This week I nearly hit my weekly AI usage quota. The ceiling is already there. AI cost is going to be a limiting factor — not eventually, but soon.&lt;/p&gt;

&lt;p&gt;The biggest AI news this week: Anthropic released &lt;a href=&quot;https://red.anthropic.com/2026/mythos-preview/&quot;&gt;Claude Mythos Preview&lt;/a&gt; and then immediately said it cannot go public. The model scored 93.9% on SWE-bench and can find zero-day vulnerabilities across every major OS and browser fast enough that Anthropic judged it too dangerous to release. Instead, they launched &lt;a href=&quot;https://www.anthropic.com/glasswing&quot;&gt;Project Glasswing&lt;/a&gt;, giving 50+ tech companies access to use Mythos for defense rather than offense.&lt;/p&gt;

&lt;p&gt;The release that excited me most was Claude channels and the Telegram plugin. I had been building toward this manually — a voice notes app to capture thoughts on Telegram, transcribe them with locally installed whisper-cpp, then copy the transcript into Claude. Three tools, fragile by design. Now that whole pipeline is native. I can talk to Claude.&lt;/p&gt;

&lt;p&gt;At work, I started reaching for Codex for deep investigations. A &lt;a href=&quot;https://github.com/anthropics/claude-code/issues/42796&quot;&gt;GitHub issue that went wide&lt;/a&gt; documented Claude Code’s regression: someone had mined months of session logs, analyzed 17,871 thinking blocks and 234,760 tool calls, and traced a quality drop back to February when Anthropic changed how thinking tokens were handled. My confirmation bias ran with it. I switched to Codex for some investigations and found it useful; it caught gaps Claude had left. Whether Claude is actually lazier or I was primed to see it that way, I cannot say with confidence. But what struck me most about that report was not the regression — it was the telemetry. Someone had built months of instrumentation around an AI agent, monitoring it like infrastructure. That is a different relationship with a tool.&lt;/p&gt;

&lt;p&gt;The permission model is a real blocker at work. I have eight terminal tabs open, each running a Claude Code or Codex session, all paused — waiting for my approval before they take the next step. The whole point of an agent is that it runs. I want to explore ways to let them operate uninterrupted without opening up everything.&lt;/p&gt;

&lt;p&gt;Last week, alongside Nokkam, I reconnected with an old friend — Sudarsh MS — who is building with AI at the same pace and intensity. We traded workflows and shared what was working. It turned into a collaboration: we wrote a quick security auditor together, which will be published here soon.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;em&gt;This post updates as new moments happen. Last updated: April 2026.&lt;/em&gt;&lt;/p&gt;
</description>
        <pubDate>Thu, 19 Mar 2026 23:47:00 +0000</pubDate>
        <link>https://srvr.in/artificial-intelligence/2026/03/19/rise-of-the-machines-watching-the-ai-big-bang-unravel/</link>
        <guid isPermaLink="true">https://srvr.in/artificial-intelligence/2026/03/19/rise-of-the-machines-watching-the-ai-big-bang-unravel/</guid>
        
        
        <category>artificial-intelligence</category>
        
      </item>
    
      <item>
        <title>Getting your feet wet with OpenTelemetry</title>
        <description>&lt;p&gt;A few months back I gave an introduction to OpenTelemetry for an engineering team. Most of it is general enough to be useful to anyone, so here it is — stripped of anything context-specific, for whoever searches for “what the hell is OTEL.”&lt;/p&gt;

&lt;p&gt;OpenTelemetry (OTEL) is an open-source observability framework: a vendor-neutral standard for instrumenting applications to emit traces, metrics, and logs. It graduated from the CNCF in 2024 and is backed by every major cloud provider and observability vendor. It is the industry standard for distributed tracing.&lt;/p&gt;

&lt;p&gt;The deck below covers the core concepts, the architecture, common pitfalls, and how to get started. Use the arrows to click through. Below the slides are links to go deeper, and a full written walkthrough for those who prefer reading.&lt;/p&gt;

&lt;hr /&gt;

&lt;div class=&quot;otel-deck&quot;&gt;

&lt;div class=&quot;otel-slides&quot;&gt;

  &lt;div class=&quot;otel-slide active&quot; data-slide=&quot;1&quot;&gt;
    &lt;div class=&quot;otel-slide-label&quot;&gt;1 / 14&lt;/div&gt;
    &lt;h2&gt;Getting your feet wet with OpenTelemetry&lt;/h2&gt;
    &lt;p class=&quot;otel-subtitle&quot;&gt;A brief introduction to distributed tracing&lt;/p&gt;
  &lt;/div&gt;

  &lt;div class=&quot;otel-slide&quot; data-slide=&quot;2&quot;&gt;
    &lt;div class=&quot;otel-slide-label&quot;&gt;2 / 14&lt;/div&gt;
    &lt;h2&gt;When logs aren&apos;t enough&lt;/h2&gt;
    &lt;p&gt;Traditional logging gets painful as your system grows:&lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;Logs are scattered across services with no shared thread&lt;/li&gt;
      &lt;li&gt;No easy way to correlate what happened across a single request&lt;/li&gt;
      &lt;li&gt;Hard to tell timing, causality, or which service is actually slow&lt;/li&gt;
      &lt;li&gt;Grep-driven debugging across six services is not a strategy&lt;/li&gt;
    &lt;/ul&gt;
    &lt;p class=&quot;otel-callout&quot;&gt;Which service is slow? Why did this request fail? Which downstream call is the bottleneck?&lt;/p&gt;
  &lt;/div&gt;

  &lt;div class=&quot;otel-slide&quot; data-slide=&quot;3&quot;&gt;
    &lt;div class=&quot;otel-slide-label&quot;&gt;3 / 14&lt;/div&gt;
    &lt;h2&gt;What is OpenTelemetry?&lt;/h2&gt;
    &lt;ul&gt;
      &lt;li&gt;An open-source observability framework for traces, metrics, and logs&lt;/li&gt;
      &lt;li&gt;Vendor-neutral — instrument once, send anywhere (Jaeger, Datadog, Grafana Tempo, AWS X-Ray)&lt;/li&gt;
      &lt;li&gt;CNCF graduated project, backed by AWS, Google, Microsoft, Datadog, Grafana&lt;/li&gt;
      &lt;li&gt;Born from the merger of OpenTracing and OpenCensus in 2019&lt;/li&gt;
    &lt;/ul&gt;
    &lt;div class=&quot;otel-diagram&quot;&gt;
      &lt;img src=&quot;https://srvr.in/assets/otel/three-pillars.svg&quot; alt=&quot;The three pillars of observability: Traces, Metrics, Logs&quot; /&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class=&quot;otel-slide&quot; data-slide=&quot;4&quot;&gt;
    &lt;div class=&quot;otel-slide-label&quot;&gt;4 / 14&lt;/div&gt;
    &lt;h2&gt;Three core concepts&lt;/h2&gt;
    &lt;div class=&quot;otel-concepts&quot;&gt;
      &lt;div class=&quot;otel-concept&quot;&gt;
        &lt;strong&gt;Trace&lt;/strong&gt;
        &lt;p&gt;The complete journey of a single request through your system — from the moment it arrives to the moment it returns.&lt;/p&gt;
      &lt;/div&gt;
      &lt;div class=&quot;otel-concept&quot;&gt;
        &lt;strong&gt;Span&lt;/strong&gt;
        &lt;p&gt;A single operation within that journey: a database query, an HTTP call, a queue publish. Spans nest to form a tree.&lt;/p&gt;
      &lt;/div&gt;
      &lt;div class=&quot;otel-concept&quot;&gt;
        &lt;strong&gt;Context Propagation&lt;/strong&gt;
        &lt;p&gt;How trace information travels across service boundaries — via the W3C &lt;code&gt;traceparent&lt;/code&gt; header. Every service adds its span and passes the context forward.&lt;/p&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;otel-diagram&quot;&gt;
      &lt;img src=&quot;https://srvr.in/assets/otel/trace-flow.svg&quot; alt=&quot;Trace flow diagram showing spans across services&quot; /&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class=&quot;otel-slide&quot; data-slide=&quot;5&quot;&gt;
    &lt;div class=&quot;otel-slide-label&quot;&gt;5 / 14&lt;/div&gt;
    &lt;h2&gt;Anatomy of a span&lt;/h2&gt;
    &lt;p&gt;Each span carries:&lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Span ID + Parent Span ID&lt;/strong&gt; — where it sits in the tree&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;Operation name&lt;/strong&gt; — what it represents (e.g., &lt;code&gt;GET /users/:id&lt;/code&gt;)&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;Timestamps&lt;/strong&gt; — start, end, duration&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;Attributes&lt;/strong&gt; — key-value pairs: HTTP method, DB query, queue topic, custom data&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;Events&lt;/strong&gt; — timestamped log entries within the span&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;Status&lt;/strong&gt; — OK, Error (with message)&lt;/li&gt;
    &lt;/ul&gt;
    &lt;div class=&quot;otel-diagram&quot;&gt;
      &lt;img src=&quot;https://srvr.in/assets/otel/span-anatomy.svg&quot; alt=&quot;Span anatomy diagram&quot; /&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class=&quot;otel-slide&quot; data-slide=&quot;6&quot;&gt;
    &lt;div class=&quot;otel-slide-label&quot;&gt;6 / 14&lt;/div&gt;
    &lt;h2&gt;Reading the waterfall view&lt;/h2&gt;
    &lt;p&gt;Every tracing backend shows traces as a waterfall (also called a flame graph). How to read it:&lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;Each horizontal bar = one span&lt;/li&gt;
      &lt;li&gt;Nested bars = parent-child relationships&lt;/li&gt;
      &lt;li&gt;Bar width = duration&lt;/li&gt;
      &lt;li&gt;Colors = different services&lt;/li&gt;
    &lt;/ul&gt;
    &lt;p&gt;At a glance you see: what happened, in what order, how long each step took, and where time was actually spent. A DB call taking 800ms of a 900ms request is immediately obvious.&lt;/p&gt;
  &lt;/div&gt;

  &lt;div class=&quot;otel-slide&quot; data-slide=&quot;7&quot;&gt;
    &lt;div class=&quot;otel-slide-label&quot;&gt;7 / 14&lt;/div&gt;
    &lt;h2&gt;The architecture&lt;/h2&gt;
    &lt;p&gt;Data flows from your service, through a collector, to a backend:&lt;/p&gt;
    &lt;p class=&quot;otel-flow&quot;&gt;Your Service → OTEL SDK/Agent → OTEL Collector → Backend (Jaeger / Datadog / Tempo / X-Ray)&lt;/p&gt;
    &lt;p&gt;The &lt;strong&gt;Collector&lt;/strong&gt; is the key piece:&lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;Centralises configuration — services just point at the collector&lt;/li&gt;
      &lt;li&gt;Redacts sensitive data before it hits any backend&lt;/li&gt;
      &lt;li&gt;Fan-out to multiple backends simultaneously&lt;/li&gt;
      &lt;li&gt;Handles buffering, retries, backpressure&lt;/li&gt;
    &lt;/ul&gt;
    &lt;div class=&quot;otel-diagram&quot;&gt;
      &lt;img src=&quot;https://srvr.in/assets/otel/architecture.svg&quot; alt=&quot;OTEL architecture diagram&quot; /&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class=&quot;otel-slide&quot; data-slide=&quot;8&quot;&gt;
    &lt;div class=&quot;otel-slide-label&quot;&gt;8 / 14&lt;/div&gt;
    &lt;h2&gt;Language support&lt;/h2&gt;
    &lt;p&gt;OTEL has mature libraries for every major language. Most auto-instrument HTTP, databases, queues, and gRPC with zero code changes:&lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Java / Scala / JVM&lt;/strong&gt;: &lt;code&gt;opentelemetry-java-instrumentation&lt;/code&gt; — attach the agent, done&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;Python&lt;/strong&gt;: &lt;code&gt;opentelemetry-python-contrib&lt;/code&gt; — wraps Django, Flask, FastAPI, SQLAlchemy, Celery&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;Node.js&lt;/strong&gt;: &lt;code&gt;opentelemetry-js&lt;/code&gt; — Express, Fastify, NestJS, pg, Redis&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;Go&lt;/strong&gt;: &lt;code&gt;opentelemetry-go&lt;/code&gt; with contrib packages&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;.NET&lt;/strong&gt;: &lt;code&gt;opentelemetry-dotnet&lt;/code&gt; — ASP.NET Core, HttpClient, SQL Client&lt;/li&gt;
    &lt;/ul&gt;
    &lt;p&gt;Check the &lt;a href=&quot;https://opentelemetry.io/ecosystem/registry/&quot; target=&quot;_blank&quot;&gt;OTEL Registry&lt;/a&gt; for your specific framework.&lt;/p&gt;
  &lt;/div&gt;

  &lt;div class=&quot;otel-slide&quot; data-slide=&quot;9&quot;&gt;
    &lt;div class=&quot;otel-slide-label&quot;&gt;9 / 14&lt;/div&gt;
    &lt;h2&gt;Getting started&lt;/h2&gt;
    &lt;p&gt;Three steps:&lt;/p&gt;
    &lt;ol&gt;
      &lt;li&gt;Set up a backend to receive traces (Jaeger is free and easy; Grafana Tempo if you&apos;re on Grafana stack)&lt;/li&gt;
      &lt;li&gt;Set up an OTEL Collector to sit in front of it&lt;/li&gt;
      &lt;li&gt;Add the SDK or agent to your service, configure via environment variables:&lt;/li&gt;
    &lt;/ol&gt;
    &lt;pre&gt;&lt;code&gt;OTEL_SERVICE_NAME=my-service
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
OTEL_TRACES_SAMPLER=parentbased_traceidratio
OTEL_TRACES_SAMPLER_ARG=0.1&lt;/code&gt;&lt;/pre&gt;
    &lt;p&gt;That last one is 10% sampling — important in production (see next slide).&lt;/p&gt;
  &lt;/div&gt;

  &lt;div class=&quot;otel-slide&quot; data-slide=&quot;10&quot;&gt;
    &lt;div class=&quot;otel-slide-label&quot;&gt;10 / 14&lt;/div&gt;
    &lt;h2&gt;Common pitfalls&lt;/h2&gt;
    &lt;ol&gt;
      &lt;li&gt;&lt;strong&gt;Missing context propagation&lt;/strong&gt; — if one service doesn&apos;t forward the &lt;code&gt;traceparent&lt;/code&gt; header, the trace breaks there. Check every service boundary.&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;100% sampling in production&lt;/strong&gt; — traces have real overhead. Start at 10%, go lower on high-traffic paths.&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;Meaningless span names&lt;/strong&gt; — &lt;code&gt;span-1&lt;/code&gt; and &lt;code&gt;handler&lt;/code&gt; tell you nothing. Name spans after what they do.&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;Sensitive data in baggage&lt;/strong&gt; — baggage propagates to every downstream service and third-party API. Don&apos;t put PII there.&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;Losing async context&lt;/strong&gt; — async code needs explicit context passing; the SDK doesn&apos;t always capture it automatically.&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;Not setting OTEL_SERVICE_NAME&lt;/strong&gt; — all your spans show up under &quot;unknown_service&quot;. Always set it.&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;Ignoring collector health&lt;/strong&gt; — a backed-up collector silently drops spans. Monitor it.&lt;/li&gt;
    &lt;/ol&gt;
  &lt;/div&gt;

  &lt;div class=&quot;otel-slide&quot; data-slide=&quot;11&quot;&gt;
    &lt;div class=&quot;otel-slide-label&quot;&gt;11 / 14&lt;/div&gt;
    &lt;h2&gt;What you can answer with traces&lt;/h2&gt;
    &lt;ul&gt;
      &lt;li&gt;Why is this request slow — and which specific operation is responsible?&lt;/li&gt;
      &lt;li&gt;Which service is the bottleneck — mine or the one I call?&lt;/li&gt;
      &lt;li&gt;Why did this request fail — and which downstream call triggered the error?&lt;/li&gt;
      &lt;li&gt;What does our actual service communication graph look like?&lt;/li&gt;
      &lt;li&gt;Is that cache actually reducing database load?&lt;/li&gt;
      &lt;li&gt;What is our p99 latency broken down by operation type?&lt;/li&gt;
    &lt;/ul&gt;
    &lt;p class=&quot;otel-callout&quot;&gt;The payoff: debug production issues in minutes, not hours. The waterfall view usually points at the problem immediately.&lt;/p&gt;
  &lt;/div&gt;

  &lt;div class=&quot;otel-slide&quot; data-slide=&quot;12&quot;&gt;
    &lt;div class=&quot;otel-slide-label&quot;&gt;12 / 14&lt;/div&gt;
    &lt;h2&gt;FAQ&lt;/h2&gt;
    &lt;dl&gt;
      &lt;dt&gt;What&apos;s the performance overhead?&lt;/dt&gt;
      &lt;dd&gt;Less than 3% CPU at 10% sampling. Most teams find it negligible. At 100% sampling on high-traffic paths it matters more.&lt;/dd&gt;
      &lt;dt&gt;Do I need to change my code?&lt;/dt&gt;
      &lt;dd&gt;Usually no. Auto-instrumentation handles HTTP, DB, queues, and gRPC. Custom spans are optional, for operations the SDK can&apos;t see.&lt;/dd&gt;
      &lt;dt&gt;How long should I keep traces?&lt;/dt&gt;
      &lt;dd&gt;3–14 days is typical. Storage is cheap, but unbounded retention gets expensive. Most debugging happens within 24–48 hours anyway.&lt;/dd&gt;
      &lt;dt&gt;Traces vs logs — when do I use which?&lt;/dt&gt;
      &lt;dd&gt;Logs are point-in-time events. Traces show timing and causality across a request. Use both — they complement each other.&lt;/dd&gt;
      &lt;dt&gt;Am I locked into a vendor?&lt;/dt&gt;
      &lt;dd&gt;No. Switching backends (Jaeger → Datadog → Tempo) is a config change, not a code change. That&apos;s the whole point.&lt;/dd&gt;
    &lt;/dl&gt;
  &lt;/div&gt;

  &lt;div class=&quot;otel-slide&quot; data-slide=&quot;13&quot;&gt;
    &lt;div class=&quot;otel-slide-label&quot;&gt;13 / 14&lt;/div&gt;
    &lt;h2&gt;Key takeaways&lt;/h2&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Traces tell stories.&lt;/strong&gt; Logs tell facts. Both matter, but a trace gives you the full picture of a request.&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;OTEL is the standard.&lt;/strong&gt; Not a vendor product, not a framework you&apos;ll need to replace in three years.&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;Auto-instrumentation gets you 80% of the value for free.&lt;/strong&gt; Add the agent, point it at a collector, and you&apos;re already useful.&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;Always sample in production.&lt;/strong&gt; 100% tracing is a footgun. Start at 10%.&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;Watch the context propagation gaps.&lt;/strong&gt; They silently break traces across service boundaries. Test early.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;

  &lt;div class=&quot;otel-slide&quot; data-slide=&quot;14&quot;&gt;
    &lt;div class=&quot;otel-slide-label&quot;&gt;14 / 14&lt;/div&gt;
    &lt;h2&gt;Where to go next&lt;/h2&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Watch first:&lt;/strong&gt; &lt;a href=&quot;https://youtube.com/watch?v=jC1icupHlMs&quot; target=&quot;_blank&quot;&gt;&quot;What Is This OpenTelemetry Thing?&quot; — Martin Thwaites, GOTO 2024&lt;/a&gt;. 46 minutes, 133k views, the clearest intro to OTEL I&apos;ve seen. If you watch nothing else, watch this.&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://opentelemetry.io/docs/&quot; target=&quot;_blank&quot;&gt;opentelemetry.io/docs&lt;/a&gt; — the official documentation. The concepts section is worth reading top to bottom.&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://opentelemetry.io/ecosystem/registry/&quot; target=&quot;_blank&quot;&gt;OTEL Registry&lt;/a&gt; — find the right instrumentation library for your language and framework.&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://www.jaegertracing.io/docs/&quot; target=&quot;_blank&quot;&gt;jaegertracing.io/docs&lt;/a&gt; — if you&apos;re self-hosting with Jaeger.&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://grafana.com/oss/tempo/&quot; target=&quot;_blank&quot;&gt;Grafana Tempo&lt;/a&gt; — a good free alternative backend if you&apos;re already on Grafana.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;

&lt;/div&gt;&lt;!-- .otel-slides --&gt;

&lt;div class=&quot;otel-controls&quot;&gt;
  &lt;button class=&quot;otel-btn&quot; id=&quot;otel-prev&quot; onclick=&quot;otelPrev()&quot;&gt;&amp;#8592; Prev&lt;/button&gt;
  &lt;span class=&quot;otel-counter&quot; id=&quot;otel-counter&quot;&gt;1 / 14&lt;/span&gt;
  &lt;button class=&quot;otel-btn&quot; id=&quot;otel-next&quot; onclick=&quot;otelNext()&quot;&gt;Next &amp;#8594;&lt;/button&gt;
&lt;/div&gt;

&lt;/div&gt;
&lt;!-- .otel-deck --&gt;

&lt;style&gt;
.otel-deck {
  margin: 2rem 0;
  font-family: inherit;
}

.otel-slides {
  background: #1e1e2e;
  color: #cdd6f4;
  border-radius: 8px;
  min-height: 400px;
  padding: 2.5rem 3rem;
  position: relative;
}

.otel-slide {
  display: none;
}

.otel-slide.active {
  display: block;
}

.otel-slide-label {
  font-size: 0.75rem;
  color: #6c7086;
  margin-bottom: 1.5rem;
  font-family: monospace;
}

.otel-slides h2 {
  color: #89b4fa;
  font-size: 1.6rem;
  margin-bottom: 1.25rem;
  border-bottom: 1px solid #313244;
  padding-bottom: 0.5rem;
}

.otel-slides p,
.otel-slides li,
.otel-slides dd {
  color: #cdd6f4;
  line-height: 1.7;
}

.otel-slides ul,
.otel-slides ol {
  padding-left: 1.5rem;
  margin: 0.75rem 0;
}

.otel-slides li {
  margin-bottom: 0.5rem;
}

.otel-slides strong {
  color: #a6e3a1;
}

.otel-slides a {
  color: #89dceb;
}

.otel-slides code {
  background: #313244;
  color: #f38ba8;
  padding: 0.15em 0.4em;
  border-radius: 3px;
  font-size: 0.9em;
}

.otel-slides pre {
  background: #313244;
  border-radius: 6px;
  padding: 1rem 1.25rem;
  margin: 1rem 0;
  overflow-x: auto;
}

.otel-slides pre code {
  background: transparent;
  color: #a6e3a1;
  padding: 0;
}

.otel-callout {
  background: #313244;
  border-left: 3px solid #89b4fa;
  padding: 0.75rem 1rem;
  margin-top: 1rem;
  border-radius: 0 4px 4px 0;
  font-style: italic;
}

.otel-subtitle {
  color: #a6adc8;
  font-size: 1.1rem;
  margin-top: 0.5rem;
}

.otel-flow {
  background: #313244;
  padding: 0.75rem 1rem;
  border-radius: 6px;
  font-family: monospace;
  font-size: 0.95rem;
  margin: 1rem 0;
  color: #f9e2af;
}

.otel-concepts {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 1rem;
  margin: 1rem 0;
}

.otel-concept {
  background: #313244;
  border-radius: 6px;
  padding: 0.9rem 1rem;
}

.otel-concept strong {
  display: block;
  margin-bottom: 0.4rem;
  color: #89b4fa;
}

.otel-concept p {
  font-size: 0.9rem;
  margin: 0;
}

.otel-diagram {
  margin-top: 1.25rem;
  text-align: center;
}

.otel-diagram img {
  max-width: 100%;
  max-height: 200px;
  border-radius: 4px;
}

.otel-slides dl {
  margin: 0.5rem 0;
}

.otel-slides dt {
  color: #89b4fa;
  font-weight: bold;
  margin-top: 0.9rem;
}

.otel-slides dd {
  margin-left: 1rem;
  margin-top: 0.25rem;
  font-size: 0.95rem;
}

.otel-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1.5rem;
  margin-top: 1rem;
  padding: 0.75rem 0;
}

.otel-btn {
  background: #313244;
  color: #cdd6f4;
  border: 1px solid #45475a;
  border-radius: 6px;
  padding: 0.5rem 1.25rem;
  cursor: pointer;
  font-size: 0.95rem;
  transition: background 0.15s;
}

.otel-btn:hover {
  background: #45475a;
}

.otel-btn:disabled {
  opacity: 0.4;
  cursor: default;
}

.otel-counter {
  font-family: monospace;
  color: #6c7086;
  font-size: 0.9rem;
  min-width: 4rem;
  text-align: center;
}

@media (max-width: 640px) {
  .otel-slides {
    padding: 1.5rem;
    min-height: 500px;
  }
  .otel-concepts {
    grid-template-columns: 1fr;
  }
}
&lt;/style&gt;

&lt;script&gt;
(function() {
  var current = 1;
  var total = 14;

  function show(n) {
    var slides = document.querySelectorAll(&apos;.otel-slide&apos;);
    slides.forEach(function(s) { s.classList.remove(&apos;active&apos;); });
    var target = document.querySelector(&apos;.otel-slide[data-slide=&quot;&apos; + n + &apos;&quot;]&apos;);
    if (target) target.classList.add(&apos;active&apos;);
    document.getElementById(&apos;otel-counter&apos;).textContent = n + &apos; / &apos; + total;
    document.getElementById(&apos;otel-prev&apos;).disabled = (n === 1);
    document.getElementById(&apos;otel-next&apos;).disabled = (n === total);
    current = n;
  }

  window.otelNext = function() { if (current &lt; total) show(current + 1); };
  window.otelPrev = function() { if (current &gt; 1) show(current - 1); };

  document.addEventListener(&apos;keydown&apos;, function(e) {
    if (e.key === &apos;ArrowRight&apos; || e.key === &apos;ArrowDown&apos;) window.otelNext();
    if (e.key === &apos;ArrowLeft&apos; || e.key === &apos;ArrowUp&apos;) window.otelPrev();
  });

  show(1);
})();
&lt;/script&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;Resources&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://youtube.com/watch?v=jC1icupHlMs&quot;&gt;What Is This OpenTelemetry Thing? — Martin Thwaites, GOTO 2024&lt;/a&gt;&lt;/strong&gt; — Start here. 46 minutes, 133k views. The clearest, most practical intro to OTEL I’ve come across. Strongly recommended.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://opentelemetry.io/docs/&quot;&gt;opentelemetry.io/docs&lt;/a&gt; — official docs; the concepts section is worth reading top to bottom&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://opentelemetry.io/ecosystem/registry/&quot;&gt;OTEL Registry&lt;/a&gt; — instrumentation libraries by language and framework&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.jaegertracing.io/&quot;&gt;Jaeger&lt;/a&gt; — the easiest open-source backend to get started with&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://grafana.com/oss/tempo/&quot;&gt;Grafana Tempo&lt;/a&gt; — good choice if you’re already on the Grafana stack&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/open-telemetry&quot;&gt;OpenTelemetry on GitHub&lt;/a&gt; — specification, SDKs, the collector&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-full-walkthrough&quot;&gt;The full walkthrough&lt;/h2&gt;

&lt;h3 id=&quot;the-problem-with-logs-alone&quot;&gt;The problem with logs alone&lt;/h3&gt;

&lt;p&gt;Picture a common scenario: a user reports a slow request. You check the logs. Service A says “request received, request completed.” Service B says “processing started, done.” Service C says “query executed.”&lt;/p&gt;

&lt;p&gt;But which service was slow? How long did each step actually take? You don’t know.&lt;/p&gt;

&lt;p&gt;Traditional logging breaks down in distributed systems. Logs scatter across services with no correlation between them. They record events but not durations. When thousands of requests run concurrently, there is no way to tell which log line belongs to which request.&lt;/p&gt;

&lt;p&gt;Distributed tracing solves this by following a single request through all your services.&lt;/p&gt;

&lt;h3 id=&quot;what-is-opentelemetry&quot;&gt;What is OpenTelemetry?&lt;/h3&gt;

&lt;p&gt;OpenTelemetry, or OTEL, is an open-source observability framework. It works with any backend — Jaeger, Datadog, Grafana Tempo, AWS X-Ray. It is a CNCF graduated project, at the same level as Kubernetes, born from the merger of OpenTracing and OpenCensus in 2019.&lt;/p&gt;

&lt;p&gt;AWS, Google, Microsoft, Datadog, and Grafana all support it. As Martin Thwaites puts it: nobody gets fired for suggesting they move their telemetry to OpenTelemetry.&lt;/p&gt;

&lt;p&gt;The key benefit is portability. Switch backends with a config change, not a code change.&lt;/p&gt;

&lt;p&gt;OpenTelemetry handles three signals: &lt;strong&gt;traces&lt;/strong&gt;, &lt;strong&gt;metrics&lt;/strong&gt;, and &lt;strong&gt;logs&lt;/strong&gt;. Traces follow a request’s journey. Metrics are measurements over time. Logs are event records. The three complement each other, but traces are the most powerful for debugging distributed systems.&lt;/p&gt;

&lt;h3 id=&quot;traces-and-spans&quot;&gt;Traces and spans&lt;/h3&gt;

&lt;p&gt;Two concepts sit at the core of tracing.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;trace&lt;/strong&gt; is the complete journey of a single request through your system. You don’t create traces directly — a trace is simply a group of spans that share the same Trace ID.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;span&lt;/strong&gt; is one operation within that journey — a database query, an HTTP call, a queue publish. Each span has a unique Span ID, a Trace ID, a Parent Span ID (linking it to whoever called it), timestamps, and attributes.&lt;/p&gt;

&lt;p&gt;One way to think about it: spans are fancy logs. Or flipped around: logs are boring traces.&lt;/p&gt;

&lt;h3 id=&quot;context-propagation&quot;&gt;Context propagation&lt;/h3&gt;

&lt;p&gt;How does the trace ID travel between services? Through context propagation.&lt;/p&gt;

&lt;p&gt;When Service A calls Service B, it passes a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;traceparent&lt;/code&gt; header. This is defined by the W3C Trace Context spec. The header carries the Trace ID, the parent Span ID, and sampling flags. Service B reads it, creates its own span as a child, and passes the header forward to any service it calls.&lt;/p&gt;

&lt;p&gt;Watch out for &lt;strong&gt;Baggage&lt;/strong&gt;. Baggage attaches arbitrary key-value data to a trace context and propagates to every downstream service — including third-party APIs. Never put sensitive data in baggage.&lt;/p&gt;

&lt;h3 id=&quot;whats-inside-a-span&quot;&gt;What’s inside a span&lt;/h3&gt;

&lt;p&gt;Each span contains:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Span ID&lt;/strong&gt; and &lt;strong&gt;Parent Span ID&lt;/strong&gt; — where it sits in the call tree&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Operation name&lt;/strong&gt; — what it represents, e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GET /api/orders&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Timestamps&lt;/strong&gt; — start time and duration&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Attributes&lt;/strong&gt; — searchable key-value metadata&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Events&lt;/strong&gt; — timestamped log entries attached to the span&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Status&lt;/strong&gt; — OK, ERROR, or UNSET&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Attributes deliver most of the searchable value. HTTP calls automatically carry method, status code, URL, and route. Database calls carry the system, statement, and database name. Messaging carries the queue system and destination. Add custom attributes for business context — user ID, order ID, tenant name — and you can search across your entire system: show me all spans where HTTP status code is 500.&lt;/p&gt;

&lt;h3 id=&quot;reading-the-waterfall&quot;&gt;Reading the waterfall&lt;/h3&gt;

&lt;p&gt;Every tracing backend visualises traces as a waterfall (sometimes called a flame graph). Each horizontal bar is one span. Nested bars show parent-child relationships. Bar width shows duration. Different colors represent different services.&lt;/p&gt;

&lt;p&gt;You see immediately where time goes and what calls what. A database call taking 800ms of a 900ms request is the widest bar — obvious at a glance. An N+1 query shows up as a row of identical small bars repeating in a loop.&lt;/p&gt;

&lt;h3 id=&quot;the-architecture&quot;&gt;The architecture&lt;/h3&gt;

&lt;p&gt;Three components work together:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OTEL SDK or Agent&lt;/strong&gt; — instruments your application, creating spans automatically for HTTP, database, messaging, and gRPC calls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OTEL Collector&lt;/strong&gt; — aggregates, processes, and routes telemetry data. Your services send to the Collector; the Collector sends to your backend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backend&lt;/strong&gt; — stores and visualises traces. Jaeger, Datadog, Grafana Tempo, AWS X-Ray, Honeycomb — your choice.&lt;/p&gt;

&lt;p&gt;The Collector is the piece worth understanding. It centralises configuration so API keys live in one place. It redacts sensitive data before it leaves your network. It fans out to multiple backends simultaneously. And if your backend is slow, it buffers rather than dropping spans.&lt;/p&gt;

&lt;h3 id=&quot;language-support&quot;&gt;Language support&lt;/h3&gt;

&lt;p&gt;OTEL has mature libraries for every major language. Most auto-instrument HTTP, databases, queues, and gRPC with zero code changes:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Java, Scala, JVM&lt;/strong&gt; — auto-instrumentation via a Java agent. Attach the agent at startup, done. HTTP clients, JDBC, Kafka, RabbitMQ, gRPC, Akka, Play all covered.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Python&lt;/strong&gt; — wrap your app with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;opentelemetry-instrument&lt;/code&gt;. Covers Django, Flask, FastAPI, requests, SQLAlchemy, Celery.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;JavaScript / TypeScript&lt;/strong&gt; — Express, Fastify, NestJS, pg, Redis auto-instrumented.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Go&lt;/strong&gt; — contrib packages for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;net/http&lt;/code&gt;, gRPC, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;database/sql&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;.NET&lt;/strong&gt; — ASP.NET Core, HttpClient, SQL Client.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check the &lt;a href=&quot;https://opentelemetry.io/ecosystem/registry/&quot;&gt;OTEL Registry&lt;/a&gt; for the full list and your specific framework.&lt;/p&gt;

&lt;h3 id=&quot;getting-started&quot;&gt;Getting started&lt;/h3&gt;

&lt;p&gt;Three steps:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Check if tracing infrastructure exists.&lt;/strong&gt; Is there an OTEL Collector running? Is there a backend like Jaeger or Datadog to send to? If not, spin up Jaeger locally — it’s a single Docker container.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Add the SDK or agent.&lt;/strong&gt; For JVM languages this means adding a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-javaagent&lt;/code&gt; flag. For Python, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pip install&lt;/code&gt; the distro and exporter then wrap with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;opentelemetry-instrument&lt;/code&gt;. For others, follow the library’s quickstart.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Configure via environment variables.&lt;/strong&gt; These are standard across all languages:&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;OTEL_SERVICE_NAME=my-service
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
OTEL_TRACES_SAMPLER=parentbased_traceidratio
OTEL_TRACES_SAMPLER_ARG=0.1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That last variable sets 10% sampling — more on why that matters below.&lt;/p&gt;

&lt;h3 id=&quot;common-pitfalls&quot;&gt;Common pitfalls&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Missing context propagation.&lt;/strong&gt; If any service in the chain doesn’t forward the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;traceparent&lt;/code&gt; header, the trace breaks there. Make sure you’re using OTEL-instrumented HTTP clients, not raw ones that strip unknown headers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;100% sampling in production.&lt;/strong&gt; Tracing has real overhead. There are two sampling strategies: head sampling decides at request start (efficient but has limited info), and tail sampling decides after the trace is complete (can selectively keep errors and slow requests). Start at 10% and tune from there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Meaningless span names.&lt;/strong&gt; Don’t call your spans &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;span-1&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;doStuff&lt;/code&gt;. Use descriptive names like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HTTP GET /api/orders&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;process-payment&lt;/code&gt;. Meaningless names make traces useless for debugging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sensitive data in attributes and baggage.&lt;/strong&gt; Baggage propagates to every downstream service, including third-party APIs. Don’t put PII, SSNs, or secrets in baggage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Losing async context.&lt;/strong&gt; In threaded or async code, spans won’t automatically connect across thread boundaries. Use your language’s context propagation APIs explicitly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not setting &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OTEL_SERVICE_NAME&lt;/code&gt;.&lt;/strong&gt; Without it, all your spans appear as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unknown_service&lt;/code&gt;. Always set it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ignoring collector health.&lt;/strong&gt; A backed-up or crashed collector silently drops spans. Monitor the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;otelcol_exporter_send_failed_spans&lt;/code&gt; metric.&lt;/p&gt;

&lt;h3 id=&quot;what-you-can-answer-with-traces&quot;&gt;What you can answer with traces&lt;/h3&gt;

&lt;p&gt;Once tracing is running, these questions have answers:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Why is this specific request slow — and which operation is responsible?&lt;/li&gt;
  &lt;li&gt;Is my service slow, or is the slowness coming from something it calls?&lt;/li&gt;
  &lt;li&gt;Why did this request fail — which downstream call threw the error?&lt;/li&gt;
  &lt;li&gt;What does our actual service communication graph look like at runtime?&lt;/li&gt;
  &lt;li&gt;Is that Redis cache actually reducing database queries?&lt;/li&gt;
  &lt;li&gt;What is our p99 latency, broken down by operation type?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The payoff: production issues debugged in minutes, not hours. Pull up the trace, and the waterfall points straight at the problem.&lt;/p&gt;
</description>
        <pubDate>Thu, 19 Mar 2026 20:00:00 +0000</pubDate>
        <link>https://srvr.in/engineering/observability/2026/03/19/getting-your-feet-wet-with-otel/</link>
        <guid isPermaLink="true">https://srvr.in/engineering/observability/2026/03/19/getting-your-feet-wet-with-otel/</guid>
        
        
        <category>engineering</category>
        
        <category>observability</category>
        
      </item>
    
      <item>
        <title>Apple Intelligence on the command line</title>
        <description>&lt;p&gt;Apple Intelligence isn’t just a suite of system-wide writing tools or a smarter Siri. For developers, it’s an on-device Foundation Models framework that we can hook into using Python.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;python-apple-fm-sdk&lt;/code&gt; &lt;sup id=&quot;fnref:1&quot;&gt;&lt;a href=&quot;#fn:1&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; provides a bridge to the ~3B parameter language model running on your Mac’s Neural Engine. It is entirely offline and requires no API keys.&lt;/p&gt;

&lt;h2 id=&quot;what-it-can-be-used-for&quot;&gt;What it can be used for&lt;/h2&gt;

&lt;p&gt;Because the model is local, it excels at high-volume, low-latency tasks where sending data to the cloud is either too slow, too expensive, or a privacy risk.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Email Classification&lt;/strong&gt;: Sorting through thousands of subject lines to flag promotions for deletion (as seen in my &lt;a href=&quot;/software/2026/03/18/a-local-ai-mailroom/&quot;&gt;previous post&lt;/a&gt;).&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Utility Content Detection&lt;/strong&gt;: Scanning your Photos library to filter out screenshots, documents, and blurry shots before cloud processing &lt;sup id=&quot;fnref:2&quot;&gt;&lt;a href=&quot;#fn:2&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Constrained Extraction&lt;/strong&gt;: Pulling dates, amounts, or names from unstructured text without paying per-token fees.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;On-Device Search&lt;/strong&gt;: Building semantic search indices for personal notes or files that never leave your machine.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;how-to-use-it&quot;&gt;How to use it&lt;/h2&gt;

&lt;p&gt;The SDK is currently in beta and requires an Apple Silicon Mac running macOS 15.0+ with Xcode 16 installed &lt;sup id=&quot;fnref:3&quot;&gt;&lt;a href=&quot;#fn:3&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;

&lt;p&gt;Implementation is centered around the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LanguageModelSession&lt;/code&gt;. It is strictly asynchronous:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;apple_fm_sdk&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fm&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;asyncio&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;classify_text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;model&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;SystemLanguageModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;is_available&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Unavailable&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;session&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;LanguageModelSession&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;prompt&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Is this SPAM or HAM? &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;
    
    &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;respond&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;prompt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;strip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;common-gotchas&quot;&gt;Common gotchas&lt;/h2&gt;

&lt;p&gt;Working with on-device models is different from calling the OpenAI or Claude APIs. Here is what I’ve encountered:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Availability Check&lt;/strong&gt;: You must always check &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;model.is_available()&lt;/code&gt;. It can fail if the model hasn’t finished downloading or if Apple Intelligence is disabled in System Settings.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Serial Execution&lt;/strong&gt;: While the Python code is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;async&lt;/code&gt;, the Apple Neural Engine (ANE) typically processes one inference request at a time. Parallelizing calls won’t make the hardware go faster.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Content Management&lt;/strong&gt;: The model is stateless. You are responsible for managing the token limit in your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LanguageModelSession&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Async Overhead&lt;/strong&gt;: Integrating it into synchronous CLI scripts often requires a bit of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;asyncio&lt;/code&gt; boilerplate or a dedicated event loop runner.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;value&quot;&gt;Value&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;/artificial-intelligence/2025/11/08/the-joy-of-ai/&quot;&gt;On-device AI changes the economics of software&lt;/a&gt;. When inference is free and private, you can apply “intelligent” logic to mundane tasks—like cleaning a mailbox or deduplicating photos—that were previously too small to justify a cloud API bill.&lt;/p&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:1&quot;&gt;
      &lt;p&gt;&lt;a href=&quot;https://github.com/apple/python-apple-fm-sdk&quot;&gt;python-apple-fm-sdk&lt;/a&gt; - The official Python interface for Apple’s Foundation Models. &lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:2&quot;&gt;
      &lt;p&gt;&lt;a href=&quot;https://machinelearning.apple.com/research/on-device-scene-analysis&quot;&gt; On-Device Scene Analysis&lt;/a&gt; - Apple’s research into using local models for utility content detection. &lt;a href=&quot;#fnref:2&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:3&quot;&gt;
      &lt;p&gt;&lt;a href=&quot;https://developer.apple.com/apple-intelligence/&quot;&gt; Apple Intelligence for Developers&lt;/a&gt; - Documentation on hardware requirements and framework adoption. &lt;a href=&quot;#fnref:3&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</description>
        <pubDate>Wed, 18 Mar 2026 00:00:00 +0000</pubDate>
        <link>https://srvr.in/software/2026/03/18/apple-intelligence-on-the-command-line/</link>
        <guid isPermaLink="true">https://srvr.in/software/2026/03/18/apple-intelligence-on-the-command-line/</guid>
        
        
        <category>software</category>
        
      </item>
    
      <item>
        <title>A Logseq skill for your Agent</title>
        <description>&lt;p&gt;Logseq is my choice for personal knowledge management. It is an offline-first, local-only outliner &lt;sup id=&quot;fnref:1&quot;&gt;&lt;a href=&quot;#fn:1&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;. When I’m working in the terminal with Gemini CLI or Claude Code, I often need to capture ideas, check my TODOs, or append a note to my daily journal.&lt;/p&gt;

&lt;p&gt;I built a skill to bridge this gap.&lt;/p&gt;

&lt;h2 id=&quot;what-it-does&quot;&gt;What it does&lt;/h2&gt;

&lt;p&gt;The skill allows the agent to interact directly with the Logseq graph on my machine. It follows the Agent Skills open standard &lt;sup id=&quot;fnref:2&quot;&gt;&lt;a href=&quot;#fn:2&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;, similar to &lt;a href=&quot;/software/2026/03/03/a-strunk-and-white-skill-for-claude-code/&quot;&gt;the Strunk and White skill&lt;/a&gt; I wrote for prose review.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Capture&lt;/strong&gt;: “Add this to my journal” or “Remember this.”&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Task Management&lt;/strong&gt;: “Show my tasks” or “Add a TODO for tomorrow.”&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Retrieval&lt;/strong&gt;: Reading specific pages or searching recent journal entries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent understands Logseq’s specific outline syntax—using tabs for nesting and bullets for blocks—and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:preferred-workflow :now&lt;/code&gt; workflow markers (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NOW&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LATER&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DONE&lt;/code&gt;).&lt;/p&gt;

&lt;h2 id=&quot;how-it-works&quot;&gt;How it works&lt;/h2&gt;

&lt;p&gt;The skill is a markdown file with clear instructions on graph structure and file naming conventions. It uses standard unix tools like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;grep&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;find&lt;/code&gt; to navigate the journals directory efficiently.&lt;/p&gt;

&lt;p&gt;Because the agent has direct access to the filesystem, it doesn’t need an API. It reads and writes the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.md&lt;/code&gt; files in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/Documents/Notes/Logseq/&lt;/code&gt; directly.&lt;/p&gt;

&lt;h2 id=&quot;getting-it&quot;&gt;Getting it&lt;/h2&gt;

&lt;p&gt;The skill is available as a &lt;a href=&quot;https://gist.github.com/srih4ri/4cb71de53e1076f0eacb4a9517252d8&quot;&gt;GitHub Gist&lt;/a&gt;. To use it:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Create a directory in your project: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.gemini/skills/logseq/&lt;/code&gt; (or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.claude/skills/logseq/&lt;/code&gt;).&lt;/li&gt;
  &lt;li&gt;Download the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SKILL.md&lt;/code&gt; file into that directory.&lt;/li&gt;
  &lt;li&gt;Activate it by asking the agent to “Read my journal.”&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The agent will then follow the instructions in the skill file to manage your notes and tasks without needing a plugin or third-party service.&lt;/p&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:1&quot;&gt;
      &lt;p&gt;&lt;a href=&quot;https://logseq.com&quot;&gt;Logseq&lt;/a&gt; - A privacy-first, open-source knowledge base. &lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:2&quot;&gt;
      &lt;p&gt;&lt;a href=&quot;https://agentskills.io&quot;&gt;Agent Skills&lt;/a&gt; - An open standard for defining tools and capabilities for AI agents. &lt;a href=&quot;#fnref:2&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</description>
        <pubDate>Wed, 18 Mar 2026 00:00:00 +0000</pubDate>
        <link>https://srvr.in/software/2026/03/18/a-logseq-skill-for-gemini-cli/</link>
        <guid isPermaLink="true">https://srvr.in/software/2026/03/18/a-logseq-skill-for-gemini-cli/</guid>
        
        
        <category>software</category>
        
      </item>
    
      <item>
        <title>A local AI mailroom</title>
        <description>&lt;p&gt;Promotional folders are digital landfills. We sign up once and receive daily “last chance” offers that bury important context. Traditional filters are too blunt; they can’t distinguish between a shipping notification and a coupon.&lt;/p&gt;

&lt;p&gt;I built a suite of Python tools to act as a personal mailroom. It uses local AI to understand, organize, and prune Gmail.&lt;/p&gt;

&lt;h2 id=&quot;how-it-works&quot;&gt;How it works&lt;/h2&gt;

&lt;p&gt;The system acts as a bridge between your inbox and local inference engines. Metadata is synced to a local database before any analysis occurs to keep the API footprint small.&lt;/p&gt;

&lt;h3 id=&quot;the-cleanup-workflow&quot;&gt;The Cleanup Workflow&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    A[Gmail Inbox] --&amp;gt;|Sync Metadata| B[(SQLite DB)]
    B --&amp;gt;|Pending Analysis| C{Local AI Engine}
    C --&amp;gt;|Apple FM SDK| D[Apple Intelligence]
    C --&amp;gt;|Ollama API| E[DeepSeek-R1]
    D --&amp;gt;|Decision: KEEP/DELETE| F[(SQLite DB)]
    E --&amp;gt;|Decision: KEEP/DELETE| F
    F --&amp;gt;|Trash Mode| G[Gmail API: Batch Modify]
    G --&amp;gt;|Move to Trash| H[Gmail Trash]
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&quot;local-first-strategy&quot;&gt;Local-first strategy&lt;/h2&gt;

&lt;p&gt;Privacy is the priority when dealing with personal email. The system uses a local-first fallback chain:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/software/2026/03/18/apple-intelligence-on-the-command-line/&quot;&gt;Apple Intelligence&lt;/a&gt;&lt;/strong&gt;: Uses the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;apple_fm_sdk&lt;/code&gt; to hook into on-device Foundation Models &lt;sup id=&quot;fnref:1&quot;&gt;&lt;a href=&quot;#fn:1&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;. It is fast, private, and requires no API keys.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;DeepSeek-R1&lt;/strong&gt;: A fallback running through Ollama &lt;sup id=&quot;fnref:2&quot;&gt;&lt;a href=&quot;#fn:2&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;. It handles complex reasoning without sending data to a cloud provider.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The AI decision engine is simple. It reads the subject and snippet, then outputs a single word: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;KEEP&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DELETE&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;smart-organization&quot;&gt;Smart organization&lt;/h2&gt;

&lt;p&gt;Manually creating Gmail filters is tedious. The system includes a discovery module that analyzes top senders and identifies patterns.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    A[SQLite DB: Sender Stats] --&amp;gt;|Top 20 Senders| B{Apple Intelligence}
    B --&amp;gt;|Analyze Patterns| C[Proposed Rule Hierarchy]
    C --&amp;gt;|User Approval| D[Gmail API: Create Labels]
    D --&amp;gt;|Create Filters| E[Gmail API: Settings/Filters]
    E --&amp;gt;|Backfill| F[Label Existing Mail]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Instead of flat filters, it proposes a hierarchy (e.g., &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cleanup/Retail/Clothing&lt;/code&gt;). Once approved, the script creates the labels and filters via the Gmail API &lt;sup id=&quot;fnref:3&quot;&gt;&lt;a href=&quot;#fn:3&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;3&lt;/a&gt;&lt;/sup&gt; and backfills existing messages.&lt;/p&gt;

&lt;h2 id=&quot;accessing-gmail&quot;&gt;Accessing Gmail&lt;/h2&gt;

&lt;p&gt;To interact with Gmail, the tool uses the Google API Client Library for Python. It follows the OAuth 2.0 flow for installed applications, requesting a specific set of scopes: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gmail.modify&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gmail.settings.basic&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gmail.labels&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On the first run, it opens a local server for browser-based authentication. The resulting credentials are saved as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;token.json&lt;/code&gt; and automatically refreshed when they expire. To minimize API latency and respect rate limits, all operations—like trashing or labeling thousands of emails—are performed using batch requests, grouping up to 1,000 operations into a single call.&lt;/p&gt;

&lt;h2 id=&quot;unsubscribe-auditing&quot;&gt;Unsubscribe auditing&lt;/h2&gt;

&lt;p&gt;The system scans for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;List-Unsubscribe&lt;/code&gt; headers and attempts to automate the process.&lt;/p&gt;

&lt;p&gt;It also logs every unsubscription in a local SQLite database. A week later, a compliance audit checks if those senders have reappeared. If they have, it flags them as a violation. It turns the “unsubscribe” button from a suggestion into a tracked requirement.&lt;/p&gt;

&lt;h2 id=&quot;value&quot;&gt;Value&lt;/h2&gt;

&lt;p&gt;The cost of implementing software requirements has dropped. This project shows that unmaintained internal processes—like managing an inbox—can now be fixed by a &lt;a href=&quot;/artificial-intelligence/2025/11/08/the-joy-of-ai/&quot;&gt;single person with an AI agent&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We are moving away from monolithic cloud services that read our data to sell ads, toward local agents that read our data to save us time.&lt;/p&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:1&quot;&gt;
      &lt;p&gt;Apple’s on-device models are part of the Apple Intelligence suite announced in 2024, focusing on privacy-preserving personal context. &lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:2&quot;&gt;
      &lt;p&gt;DeepSeek-R1 is an open-weights reasoning model that achieved parity with proprietary models in early 2025. &lt;a href=&quot;#fnref:2&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:3&quot;&gt;
      &lt;p&gt;The Gmail API allows for fine-grained control over labels and filters through the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;users.settings.filters&lt;/code&gt; resource. &lt;a href=&quot;#fnref:3&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</description>
        <pubDate>Wed, 18 Mar 2026 00:00:00 +0000</pubDate>
        <link>https://srvr.in/software/2026/03/18/a-local-ai-mailroom/</link>
        <guid isPermaLink="true">https://srvr.in/software/2026/03/18/a-local-ai-mailroom/</guid>
        
        
        <category>software</category>
        
      </item>
    
  </channel>
</rss>
