A self-translating blog: SQLite, a Markdown editor, and a local LLM
This blog has a slightly unusual property: I write a post once, in English, and a local language model produces the Russian and Ukrainian versions. Here's the machinery — and, as always, where I deliberately kept the model out of the critical path.
Posts are data, not files
The first blog version stored each post as a Markdown file compiled at build
time. Nice for a developer, useless for editing on the fly — every fix meant a
rebuild and a deploy. So posts moved into SQLite, one row per
(slug, language), and the Markdown is rendered to HTML at request time.
That single change unlocked everything else: an admin editor. Behind the
passkey-only /admin, there's a plain Markdown editor — title, slug, tags, a big
body field, and a Published toggle. Writing or fixing a post is now a textarea
and a Save button, no build step in sight.
One button, two languages
The site is trilingual (EN / RU / UK), and keeping three hand-written copies of every post in sync is exactly the kind of chore that rots. So next to each English post there's a “Translate → RU & UK” button.
Press it and the server hands the post to a local Gemma model (via Ollama on
the LAN — no text leaves the network). A strict prompt tells it to translate the
title, description and body while keeping all Markdown, code blocks, URLs and
technical terms (SQLite, BGP, file names) exactly as written. The
translation runs in the background — language models take their time — and the
result is saved as a draft.
That word matters. Translations never publish themselves. They land as drafts so I can read them, fix the odd clumsy phrase (Ukrainian especially), and only then flip them live. The model writes a first pass; a human signs off.
The reader's side
Each post ends up at three URLs — /blog/…, /ru/blog/…, /uk/blog/… — with a
small language switcher in the article that only shows the versions that are
actually published. Every page carries its own canonical URL, BlogPosting
structured data in its language, and an entry in the sitemap, so search engines
see three proper localized articles rather than one page with a query string.
And because honesty is cheap and trust is not, every post and the blog index carry a short transparency note: most posts are drafted with AI assistance, and the RU/UK versions are machine-translated and reviewed.
The same principle, again
If you've read the other posts here you'll recognise the shape. The LLM is a drafting and translation assistant, not the source of truth — the SQLite row is. It runs off to the side, on demand, producing drafts a human approves; if Gemma were down, the blog wouldn't notice, I'd just not have a fresh translation yet.
A language model is a wonderful way to start a sentence in another language. It's a poor place to end the editorial decision. This blog is built around that line.
Transparency: most posts here are drafted with AI assistance, and the non-English versions are machine-translated by a local LLM and then reviewed. Spotted an error? Please let me know.