<?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: Mario Hahn</title>
    <description>The latest articles on DEV Community by Mario Hahn (@mario_hahn_69c72ae001e781).</description>
    <link>https://dev.to/mario_hahn_69c72ae001e781</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%2F4217968%2F7ac3afff-ee01-4b96-9026-07f6a5f9a403.png</url>
      <title>DEV Community: Mario Hahn</title>
      <link>https://dev.to/mario_hahn_69c72ae001e781</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mario_hahn_69c72ae001e781"/>
    <language>en</language>
    <item>
      <title>I let my coding agent do the whole localization job, from messy keys to approved translations</title>
      <dc:creator>Mario Hahn</dc:creator>
      <pubDate>Fri, 30 Oct 2026 21:05:19 +0000</pubDate>
      <link>https://dev.to/mario_hahn_69c72ae001e781/i-let-my-coding-agent-do-the-whole-localization-job-from-messy-keys-to-approved-translations-2adc</link>
      <guid>https://dev.to/mario_hahn_69c72ae001e781/i-let-my-coding-agent-do-the-whole-localization-job-from-messy-keys-to-approved-translations-2adc</guid>
      <description>&lt;p&gt;Every app I ship exists on iOS and Android, some on the web too. After a few releases the&lt;br&gt;
strings were a mess:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;half of them hard-coded in SwiftUI views and Compose screens&lt;/li&gt;
&lt;li&gt;the rest in &lt;code&gt;Localizable.xcstrings&lt;/code&gt; under keys like &lt;code&gt;"Sign in"&lt;/code&gt;, &lt;code&gt;account.login&lt;/code&gt; and
&lt;code&gt;login_button&lt;/code&gt;, all for the same button&lt;/li&gt;
&lt;li&gt;the Android app with its own names for everything (&lt;code&gt;R.string.login_btn&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;the web app with a third set (&lt;code&gt;auth.signIn&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three names for one text means three translations, three chances to drift, and nobody knows&lt;br&gt;
which one is right.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I wanted
&lt;/h2&gt;

&lt;p&gt;Say "localize this app" to my coding agent, and get it done &lt;strong&gt;properly&lt;/strong&gt;: not a quick&lt;br&gt;
translation that breaks a placeholder or puts a German sentence into a toolbar button.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://locli.dev" rel="noopener noreferrer"&gt;locli&lt;/a&gt;, and now this is what my agent (Claude Code, but Cursor&lt;br&gt;
and Codex work the same) does on its own.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. Move every text into the catalog
&lt;/h2&gt;

&lt;p&gt;Hard-coded strings go into the app's own catalog: the String Catalog on iOS, &lt;code&gt;strings.xml&lt;/code&gt; on&lt;br&gt;
Android, ICU JSON on the web or backend. The code switches to looking them up by key.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. One identifier per meaning, on every platform
&lt;/h2&gt;

&lt;p&gt;Every text gets one &lt;code&gt;lower_snake_case&lt;/code&gt; identifier with a feature prefix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;account_login
settings_language_title
cart_checkout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;same identifier on iOS, Android and web&lt;/strong&gt;. So "Sign in" is one key, translated once,&lt;br&gt;
used everywhere. Where a platform really needs different wording, it gets a variant of that&lt;br&gt;
key instead of a new one.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Merge the duplicates
&lt;/h2&gt;

&lt;p&gt;This is the part I used to dread. The agent finds keys that mean the same thing&lt;br&gt;
(&lt;code&gt;account.login&lt;/code&gt; on iOS, &lt;code&gt;account_login&lt;/code&gt; on Android), merges them into one, keeps every&lt;br&gt;
translation, and switches the code to the new name.&lt;/p&gt;

&lt;p&gt;It only stops and asks me where two values really conflict, for example two different German&lt;br&gt;
texts for the same button. It never just picks one.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. Translate, with context
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;locli push&lt;/code&gt; uploads the keys and texts (it never overwrites what a translator changed). Then&lt;br&gt;
the agent translates, using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the project's &lt;strong&gt;tone&lt;/strong&gt; per language and a &lt;strong&gt;glossary&lt;/strong&gt; ("workspace", never "project")&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;where each text is used&lt;/strong&gt;: &lt;code&gt;locli context&lt;/code&gt; reads the code and knows that &lt;code&gt;cart_checkout&lt;/code&gt; is a
toolbar item and &lt;code&gt;cart_title&lt;/code&gt; a screen title&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3wzol4snrpahtpxyveet.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3wzol4snrpahtpxyveet.png" alt="locli context output" width="800" height="279"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every upload is checked: placeholders like &lt;code&gt;%lld&lt;/code&gt; or &lt;code&gt;{count}&lt;/code&gt; in every language, plural forms&lt;br&gt;
per language (Polish and Arabic need more than "one" and "other"), glossary terms.&lt;/p&gt;
&lt;h2&gt;
  
  
  5. I approve, then it ships
&lt;/h2&gt;

&lt;p&gt;The one thing the agent can't do is ship. I go through the changed texts in the locli web app,&lt;br&gt;
each with a diff, and approve the release. Only then does &lt;code&gt;locli pull&lt;/code&gt; write exactly that&lt;br&gt;
release into the app. Builds stay reproducible, and a bad translation never reaches the store&lt;br&gt;
just because an agent was confident.&lt;/p&gt;
&lt;h2&gt;
  
  
  Six apps in
&lt;/h2&gt;

&lt;p&gt;I run six of my own apps on it, about 2,650 keys across iOS, Android, web and backend, and&lt;br&gt;
locli localizes its own web app. Dogfooding found real bugs, and each got a fix and a test:&lt;br&gt;
current Xcode saves String Catalogs as version 1.1, which the CLI refused at first.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;In your app's repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @locli/cli init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or tell your agent: &lt;em&gt;"Set up locli for this app's texts."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For Claude Code there's a plugin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/plugin marketplace add mariohahn/locli
/plugin install locli@locli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI and MCP server are MIT and have no dependencies:&lt;br&gt;
&lt;a href="https://github.com/mariohahn/locli" rel="noopener noreferrer"&gt;github.com/mariohahn/locli&lt;/a&gt;. One project is free.&lt;/p&gt;

&lt;p&gt;I'd love to hear how messy your keys are, and where this breaks on your project.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>android</category>
      <category>localization</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
