<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nikolai Fedotenko</title>
    <description>The latest articles on DEV Community by Nikolai Fedotenko (@nfedotenko).</description>
    <link>https://dev.to/nfedotenko</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4182944%2Ff9d8c979-7f8b-416f-b00c-572590893334.jpg</url>
      <title>DEV Community: Nikolai Fedotenko</title>
      <link>https://dev.to/nfedotenko</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nfedotenko"/>
    <language>en</language>
    <item>
      <title>I built a localization pass that never lets an unreleased game script leave the laptop</title>
      <dc:creator>Nikolai Fedotenko</dc:creator>
      <pubDate>Wed, 14 Oct 2026 21:40:12 +0000</pubDate>
      <link>https://dev.to/nfedotenko/i-built-a-localization-pass-that-never-lets-an-unreleased-game-script-leave-the-laptop-n80</link>
      <guid>https://dev.to/nfedotenko/i-built-a-localization-pass-that-never-lets-an-unreleased-game-script-leave-the-laptop-n80</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12"&gt;Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;locsmith&lt;/strong&gt; is a command-line tool that pre-translates a game's string table with an open-weight model running locally, then checks every line before it can break the build.&lt;/p&gt;

&lt;p&gt;I work in game development, and the localization step is where small studios get hurt. The script of an unannounced game is under NDA, so pasting it into a cloud chatbot is off the table. An agency pass on every content update costs more than many indie teams make. And raw machine translation breaks things in ways that only show up in QA: a &lt;code&gt;{price}&lt;/code&gt; placeholder that became &lt;code&gt;{цена}&lt;/code&gt;, a &lt;code&gt;&amp;lt;/color&amp;gt;&lt;/code&gt; tag that disappeared, or a button label that's twice as wide as the button.&lt;/p&gt;

&lt;p&gt;locsmith handles the boring 80% before a human translator sees the file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It batches strings to a local model with context hints ("Blacksmith NPC, gruff but friendly") and the glossary terms relevant to that batch.&lt;/li&gt;
&lt;li&gt;It validates every result: placeholders and rich-text tags must match the source exactly (reordering is allowed), line breaks are preserved, per-string &lt;code&gt;max_len&lt;/code&gt; is respected, and glossary terms are used.&lt;/li&gt;
&lt;li&gt;If a string fails, it goes back to the model with the specific problem ("lost placeholder {price}", "too long: 32 &amp;gt; 28") and is retried. Strings that still fail are marked &lt;code&gt;review&lt;/code&gt; instead of silently shipping.&lt;/li&gt;
&lt;li&gt;Reruns only translate new or changed strings, so it fits a daily-build workflow. A non-zero exit code means "human needed", which lets it gate a CI step.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Who would pay:&lt;/strong&gt; solo devs and 2–10 person studios shipping to Steam in several languages. They'd pay for a one-time license or a small subscription that turns "we'll localize after launch" into "every build has a draft translation and a list of the 20 lines a human must look at".&lt;/p&gt;

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

&lt;p&gt;One command against the bundled sample (13 strings, English → Russian, with a glossary):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python3 locsmith.py samples/strings.csv &lt;span class="nt"&gt;--to&lt;/span&gt; ru &lt;span class="nt"&gt;-g&lt;/span&gt; samples/glossary.ru.csv
&lt;span class="go"&gt;translated 13/13

gemma4:12b @ http://localhost:11434/v1
strings: 13  ok: 11  needs review: 2  auto-fixed on retry: 0  cached: 0
time: 200.2s  (15.40s per new string)
&lt;/span&gt;&lt;span class="gp"&gt;  REVIEW hud.stamina_low: too long: 41 &amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;24 chars
&lt;span class="gp"&gt;  REVIEW shop.buy: too long: 32 &amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;28 chars
&lt;span class="go"&gt;written: samples/strings.ru.csv
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The output CSV is ready to drop into the build; the two &lt;code&gt;review&lt;/code&gt; rows are flagged, not shipped:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;key&lt;/th&gt;
&lt;th&gt;source&lt;/th&gt;
&lt;th&gt;target&lt;/th&gt;
&lt;th&gt;status&lt;/th&gt;
&lt;th&gt;issues&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;shop.buy&lt;/td&gt;
&lt;td&gt;Buy for {price} Aether Shards&lt;/td&gt;
&lt;td&gt;Купить за {price} Осколков Эфира&lt;/td&gt;
&lt;td&gt;review&lt;/td&gt;
&lt;td&gt;too long: 32 &amp;gt; 28 chars&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;shop.not_enough&lt;/td&gt;
&lt;td&gt;You need {0} more Aether Shards.&lt;/td&gt;
&lt;td&gt;Вам нужно еще {0} Осколков Эфира.&lt;/td&gt;
&lt;td&gt;ok&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;loot.picked&lt;/td&gt;
&lt;td&gt;You picked up %d x %s&lt;/td&gt;
&lt;td&gt;Вы подобрали %d x %s&lt;/td&gt;
&lt;td&gt;ok&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;quest.reward&lt;/td&gt;
&lt;td&gt;[b]Reward:[/b] {xp} XP and {shards} Aether Shards&lt;/td&gt;
&lt;td&gt;[b]Награда:[/b] {xp} ОЗП и {shards} Осколков Эфира&lt;/td&gt;
&lt;td&gt;ok&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;npc.smith.greet&lt;/td&gt;
&lt;td&gt;Back again, Warden? ...&lt;/td&gt;
&lt;td&gt;Снова ты, Страж? Кузница не спит, и я тоже.&lt;/td&gt;
&lt;td&gt;ok&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every placeholder (&lt;code&gt;{price}&lt;/code&gt;, &lt;code&gt;{0}&lt;/code&gt;, &lt;code&gt;%d&lt;/code&gt;, &lt;code&gt;%s&lt;/code&gt;, &lt;code&gt;{xp}&lt;/code&gt;, &lt;code&gt;{shards}&lt;/code&gt;, &lt;code&gt;{key_dodge}&lt;/code&gt;), rich-text tag (&lt;code&gt;&amp;lt;color=#ff4040&amp;gt;&lt;/code&gt;, &lt;code&gt;[b]&lt;/code&gt;) and &lt;code&gt;\n&lt;/code&gt; survived, and the glossary terms came through inflected — &lt;em&gt;Осколок Эфира&lt;/em&gt; → &lt;em&gt;Осколков Эфира&lt;/em&gt;, &lt;em&gt;Warden&lt;/em&gt; → &lt;em&gt;Страж&lt;/em&gt;. The only two flags are genuine UI-length overflows a human (or a shorter rephrase) needs to resolve.&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/nfedotenko" rel="noopener noreferrer"&gt;
        nfedotenko
      &lt;/a&gt; / &lt;a href="https://github.com/nfedotenko/locsmith" rel="noopener noreferrer"&gt;
        locsmith
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Local, private, validated pre-translation of game strings with an open-weight model (Gemma via Ollama)
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;locsmith&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;Local, private pre-translation of game strings with an open-weight model.&lt;/p&gt;

&lt;p&gt;Indie teams can't send an unannounced game's script to a cloud API (NDAs, leaks) and can't afford
an agency pass on every build. &lt;code&gt;locsmith&lt;/code&gt; runs a first translation pass on your own machine
then &lt;strong&gt;validates every string&lt;/strong&gt; so the output is safe to drop into the build:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;placeholders and tags survive untouched: &lt;code&gt;{0}&lt;/code&gt;, &lt;code&gt;{name}&lt;/code&gt;, &lt;code&gt;%s&lt;/code&gt;, &lt;code&gt;%1$s&lt;/code&gt;, &lt;code&gt;&amp;lt;color=#f00&amp;gt;&lt;/code&gt;, &lt;code&gt;[b]&lt;/code&gt;, &lt;code&gt;\n&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;UI length limits (&lt;code&gt;max_len&lt;/code&gt; per string)&lt;/li&gt;
&lt;li&gt;glossary terms (character names, currencies) used consistently; inflected languages supported via stems, &lt;code&gt;strict&lt;/code&gt; for exact names&lt;/li&gt;
&lt;li&gt;broken strings are sent back to the model with the exact problem, up to &lt;code&gt;--retries&lt;/code&gt; times; the rest are flagged &lt;code&gt;review&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;reruns only translate new or changed strings&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Works with any OpenAI-compatible endpoint: Ollama, llama.cpp &lt;code&gt;llama-server&lt;/code&gt;, vLLM, LM Studio. Standard library only.&lt;/p&gt;
&lt;p&gt;Gemma 4 and other reasoning models "think" before…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/nfedotenko/locsmith" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model:&lt;/strong&gt; Gemma 4 (&lt;code&gt;gemma4:12b&lt;/code&gt;, Q4_K_M) served by Ollama on a MacBook Pro (Apple M1, 16 GB). The tool talks to any OpenAI-compatible endpoint, so llama.cpp's &lt;code&gt;llama-server&lt;/code&gt;, vLLM or LM Studio work too. Switching models is a single flag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reasoning off by default.&lt;/strong&gt; Gemma 4 is a thinking model: left alone it spent ~1,700–3,000 tokens &lt;em&gt;reasoning&lt;/em&gt; about a 13-string batch before emitting any JSON, which on a 16 GB laptop meant minutes per batch. locsmith sends &lt;code&gt;reasoning_effort: none&lt;/code&gt;, which drops the same batch to ~310 generated tokens with identical output quality — about a 10x cut. &lt;code&gt;--reasoning low|medium|high&lt;/code&gt; turns deliberation back on when you want it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero dependencies:&lt;/strong&gt; a single Python file using only the standard library, so a studio's build machine doesn't need another package manager.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validation is the product.&lt;/strong&gt; A single regex tokenizer recognizes &lt;code&gt;{0}&lt;/code&gt;/&lt;code&gt;{name}&lt;/code&gt;, printf (&lt;code&gt;%s&lt;/code&gt;, &lt;code&gt;%1$s&lt;/code&gt;, &lt;code&gt;%.2f&lt;/code&gt;), HTML-like tags (&lt;code&gt;&amp;lt;color=#f00&amp;gt;&lt;/code&gt;), BBCode (&lt;code&gt;[b]&lt;/code&gt;) and literal escapes. Source and translation are compared as multisets, so a language can move &lt;code&gt;%s&lt;/code&gt; before &lt;code&gt;%d&lt;/code&gt; but can't lose or invent one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Glossary in inflected languages:&lt;/strong&gt; in Russian, "Aether Shard" can appear as &lt;em&gt;осколок&lt;/em&gt;, &lt;em&gt;осколков&lt;/em&gt; or &lt;em&gt;осколками&lt;/em&gt;. Exact matching produces endless false alarms, so by default each glossary word is matched by its stem. Proper names can be marked &lt;code&gt;strict&lt;/code&gt; instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests&lt;/strong&gt; cover the validators, JSON recovery from chatty model output, the retry loop and the incremental cache.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Results on the bundled sample (13 strings, en → ru, &lt;code&gt;gemma4:12b&lt;/code&gt; via Ollama):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;passed validation on the first try: 11 / 13&lt;/li&gt;
&lt;li&gt;auto-fixed on retry: 0&lt;/li&gt;
&lt;li&gt;left for human review: 2 (both genuine UI-length overflows, not model mistakes)&lt;/li&gt;
&lt;li&gt;time: 200 s total, ~15 s/string on a 16 GB M1 — memory-bound, since a 12B model barely fits; the first-pass batch of all 13 runs in ~31 s warm, and the retries for the two length failures dominate the rest. A machine with more RAM, or a smaller Gemma, is considerably faster.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tiger Data: asking the catalog a question
&lt;/h2&gt;

&lt;p&gt;The translator stays on the laptop. After a pass, a loc lead still needs a dashboard question: &lt;em&gt;which Russian UI strings actually overflow &lt;code&gt;max_len&lt;/code&gt;, and which keys carry glossary terms?&lt;/em&gt; That is ordinary SQL, not another LLM call, so I loaded the public sample catalog onto a Tiger Cloud TimescaleDB service (&lt;code&gt;locsmith-dev&lt;/code&gt;: PostgreSQL 18.6, TimescaleDB 2.30.2) through the Tiger MCP and asked it there.&lt;/p&gt;

&lt;p&gt;The overflow query returned in ~200 ms and agreed with locsmith's &lt;code&gt;review&lt;/code&gt; flags — independently, without dumping rows into chat:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;key&lt;/th&gt;
&lt;th&gt;Russian&lt;/th&gt;
&lt;th&gt;max&lt;/th&gt;
&lt;th&gt;chars&lt;/th&gt;
&lt;th&gt;over by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hud.stamina_low&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Мало выносливости! (+ color tags)&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;41&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+17&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;shop.buy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Купить за &lt;code&gt;{price}&lt;/code&gt; Осколки Эфира&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+3&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;achv.first_blood&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Первая кровь&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;fits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ui.play&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Играть&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;fits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ui.settings&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Настройки&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;fits&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A join against the glossary table showed coverage too: &lt;em&gt;Aether Shard&lt;/em&gt; → Осколок Эфира on &lt;code&gt;shop.buy&lt;/code&gt;, &lt;code&gt;shop.not_enough&lt;/code&gt;, &lt;code&gt;quest.reward&lt;/code&gt;; &lt;em&gt;Stamina&lt;/em&gt; on the HUD and tutorial; &lt;em&gt;Warden&lt;/em&gt; (&lt;code&gt;strict&lt;/code&gt;) on the blacksmith greeting; &lt;em&gt;Hollow&lt;/em&gt; on the achievement description.&lt;/p&gt;

&lt;p&gt;The unreleased script never goes to Tiger. Only keys, lengths, status, and glossary stems — the same metadata a studio already puts in a loc dashboard. TimescaleDB is still Postgres here; hypertables would matter later for per-build translation runs as time series. That split is the product: Gemma translates privately, Tiger answers the catalog.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;For this product, an open model is a requirement, not just a preference.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Data never leaves the machine.&lt;/strong&gt; An unannounced game's dialogue, item names and plot twists are what the NDA protects. With a local open-weight model, the leak surface is the developer's own laptop. I can say that to a studio with a straight face. "We send it to an API, but they promise not to train on it" doesn't pass a publisher's legal review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The unit economics work.&lt;/strong&gt; A mid-size indie game has tens of thousands of strings, and every patch adds more. With local inference, the marginal cost of a rerun is electricity. That's what makes "translate every build" a feature instead of a budget meeting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No lock-in, room to specialize.&lt;/strong&gt; Games have their own vocabulary and tone. With open weights, a studio can swap in a bigger model for the story and a fast one for UI, or fine-tune on its previously approved translations. Nobody can deprecate the model under them mid-production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Works offline.&lt;/strong&gt; Build machines, trains, conventions, game jams.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The trade-off is that a local 12B model makes more mistakes than a frontier API. That's exactly why the validation and retry loop exists: the tool doesn't trust the model, it checks it. And why the catalog lives in Postgres after the pass: the questions a loc lead asks ("what overflowed this build?") should be SQL, not another prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Best Use of Gemma&lt;/li&gt;
&lt;li&gt;Best Use of Tiger Data&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>gamedev</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
