The idea
A small server that owns every user-facing string in your app. It ingests strings.xml, .xcstrings, i18next JSON, and gettext .po files from your repo, stores each key with its source text and all translations, and keeps a translation memory of everything you've ever shipped. When a new key appears, it drafts a translation using the closest prior matches plus a project glossary, then holds it in a "needs review" state until a human approves. A companion CLI runs in CI and fails the build when a key is missing, stale, or contains a broken interpolation placeholder.
Why build this
Solo developers and two-person teams ship in five languages because the app stores make it cheap to do so, then immediately lose control of it. Strings drift out of sync with the code, one language gets a feature and the others don't, and a %1$s gets dropped in a hand-edited file and crashes the app on Turkish devices only. The commercial localization platforms — Lokalise, Crowdin, Phrase — solve this well but price per seat and per string in a way that stops making sense below a real revenue line, and they want your source strings on their servers.
The timing is good for two reasons. Machine translation has crossed the threshold where a glossary-constrained draft is genuinely a starting point rather than a joke, especially for short UI strings where the hard part is terminology consistency, not fluency. And the file formats have mostly converged — Apple's .xcstrings catalog and ICU MessageFormat cover a large share of what a modern app emits, so a parser layer is a weekend of work rather than a quarter.
Stack sketch
- Server: Go with
chi, single binary, SQLite by default and Postgres for teams that want it. - Fuzzy matching: trigram similarity via SQLite FTS5, or
pg_trgmon Postgres — vector search is overkill for 40-character UI strings and does worse on near-duplicates. - Format parsers:
encoding/xmlfor Android, Apple's.xcstringsJSON schema, andgithub.com/leonelquinteros/gotextfor.po. - Placeholder validation: parse ICU MessageFormat with
go-i18n's plural rules; assert the placeholder set in the translation is exactly the source's. - Drafting: Claude API, one request per batch of keys, with the glossary and the top three translation-memory matches injected into the prompt.
- CLI: same Go module, distributed as
tmctl, wired into CI as a singletmctl checkstep. - Web UI: SvelteKit, server-rendered, one review screen per language.
Scope for v1
- Two formats only: Android
strings.xmland i18next JSON. - Push and pull commands:
tmctl pushuploads source strings,tmctl pullwrites translated files back to the repo. tmctl checkexits nonzero on missing keys or placeholder mismatches.- Glossary as a flat term list per project.
- LLM drafting behind a flag, off by default, with the reviewer UI as the only path to
approved. - Deliberately out: branching and merge workflows, screenshot context, TMX import/export, per-user permissions, webhooks.
Where it could go
The obvious next step is context. UI strings translate badly in isolation — "Open" is a verb or an adjective depending on the screen — so attaching a screenshot and the surrounding keys to each entry lifts draft quality more than any model upgrade will. A capture step in your existing screenshot test suite can supply that automatically.
After that, the translation memory itself becomes the product. Once a project has a few thousand approved pairs, you can offer a pre-flight score on new strings — "this phrasing has never appeared before, expect review time" — and flag source strings that are near-duplicates of existing ones, which is the cheapest localization saving there is. A hosted tier for people who don't want to run it is the natural revenue path, since the self-hosted binary does the marketing.
Watch out for
Placeholder validation is where the real value lives, and it's harder than it looks: Android, iOS, and ICU each have their own syntax and their own plural category rules, and a naive regex will pass strings that crash at runtime. Get the parser right before you get the LLM drafting right.