<?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: 10coding</title>
    <description>The latest articles on DEV Community by 10coding (@10coding).</description>
    <link>https://dev.to/10coding</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%2F4157283%2F248fe73e-ff29-42f6-b025-77affeb9ed10.jpg</url>
      <title>DEV Community: 10coding</title>
      <link>https://dev.to/10coding</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/10coding"/>
    <language>en</language>
    <item>
      <title>I dumped semantic i18n keys. Now the source text itself is the key — and LLMs do the rest.</title>
      <dc:creator>10coding</dc:creator>
      <pubDate>Mon, 09 Nov 2026 10:40:48 +0000</pubDate>
      <link>https://dev.to/10coding/i-dumped-semantic-i18n-keys-now-the-source-text-itself-is-the-key-and-llms-do-the-rest-i04</link>
      <guid>https://dev.to/10coding/i-dumped-semantic-i18n-keys-now-the-source-text-itself-is-the-key-and-llms-do-the-rest-i04</guid>
      <description>&lt;h2&gt;
  
  
  The thing everyone does (and why we stopped)
&lt;/h2&gt;

&lt;p&gt;If you've ever built a non-trivial UI in a second language, you've written code like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;features.localeFiles.title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;features.localeFiles.description&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Semantic keys. Dot-separated, namespaced, "proper". It's the pattern every i18n library teaches you: i18next, vue-i18n, react-i18next, all of them. You write a key, you look up the translation in a JSON file, done.&lt;/p&gt;

&lt;p&gt;For a long time I did the same thing. Then I hit a wall: our engineers write the UI copy themselves, we're shipping to multiple languages, and we're using LLMs to do the bulk of the translation. In that world, semantic keys started to feel like pure overhead.&lt;/p&gt;

&lt;p&gt;So I tried something that sounds weird at first: &lt;strong&gt;use the source string itself as the key.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Language resource files&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Translations live in src/locales/*.json — the key is the source string.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;features.localeFiles.title&lt;/code&gt;. No jumping between files to remember what a key meant. The source string is the source of truth, and the locale file is just &lt;code&gt;{ "&amp;lt;source&amp;gt;": "&amp;lt;translation&amp;gt;" }&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The source language can be English, Chinese, Spanish — whatever your team writes the product in. The target locales are whatever you ship to. In our case the source is English and we're translating to Spanish (next up: Japanese), but the mechanism doesn't care.&lt;/p&gt;

&lt;p&gt;This isn't a new idea — lots of teams do it — but what I haven't seen much of is a proper CLI workflow around it, especially one that integrates LLMs without losing the safety net of CI checks. So I built one: &lt;a href="https://www.npmjs.com/package/@10coding/i18n-trans" rel="noopener noreferrer"&gt;&lt;code&gt;@10coding/i18n-trans&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let me walk through the problems with semantic keys, the runtime contract I settled on, and the three commands that actually run the workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why semantic keys started to hurt us
&lt;/h2&gt;

&lt;p&gt;Semantic keys aren't &lt;em&gt;wrong&lt;/em&gt;. They're a great fit for teams with a dedicated localization team, where copy is written by product managers in a CMS and engineers never see the source string. But if your engineers maintain both the code &lt;em&gt;and&lt;/em&gt; the translations, the costs add up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Code and copy live in two places.&lt;/strong&gt; Reading &lt;code&gt;features.localeFiles.title&lt;/code&gt; tells you nothing. You have to open the JSON file, find the key, and only then remember what the screen says.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLMs translate badly without context.&lt;/strong&gt; When you hand a model the key &lt;code&gt;cta.book&lt;/code&gt; and the source "Book", it has no idea whether this is "a book you're reading" or "book a flight". It'll happily give you "Libro" in both cases — wrong for one of them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One word, two translations, one key.&lt;/strong&gt; English "Book" is sometimes &lt;code&gt;Libro&lt;/code&gt;, sometimes &lt;code&gt;Reservar&lt;/code&gt;. A single semantic key can only hold one translation. You end up inventing &lt;code&gt;cta.book_noun&lt;/code&gt; and &lt;code&gt;cta.book_verb&lt;/code&gt; to disambiguate — for a single English word.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing keys and dead keys accumulate.&lt;/strong&gt; Code references a key that isn't in the JSON, or the JSON has a key that no code uses anymore. Without tooling, you don't find out until a user sees the raw key on screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-framework pain.&lt;/strong&gt; React uses i18next, Vue uses vue-i18n. Different key conventions, different interpolation syntax. Sharing locale files across both is a mess.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The source-text-as-key approach doesn't fix all of these. But it turns the ones it &lt;em&gt;does&lt;/em&gt; fix into trivial problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data format
&lt;/h2&gt;

&lt;p&gt;It's deliberately boring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The source locale file is an identity map — source → source.&lt;/li&gt;
&lt;li&gt;Target locale files use the exact same keys; the value is the translation.&lt;/li&gt;
&lt;li&gt;Keys are flat strings. No nesting, no dot-separated namespaces.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;src/locales/en-US.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Hello, {name}!"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hello, {name}!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Book"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Book"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;src/locales/es-ES.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Hello, {name}!"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"¡Hola, {name}!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Book"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Libro"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Book__TRANS_ID_reserve"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Reservar"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call sites look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello, {name}!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Ada&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Book&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                    &lt;span class="c1"&gt;// Libro (a book you read)&lt;/span&gt;
&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Book&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reserve&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;         &lt;span class="c1"&gt;// Reservar (book a flight / a table)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The wins are immediate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The code &lt;em&gt;is&lt;/em&gt; the copy.&lt;/strong&gt; Open a component, read the source string, you know exactly what the screen says.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The source text is already the LLM input.&lt;/strong&gt; No separate "key → source string" lookup table to maintain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flat JSON&lt;/strong&gt; means diffs are trivial, dead keys are obvious.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;__TRANS_ID&lt;/code&gt; suffixes&lt;/strong&gt; let one source string have multiple translations for different contexts (more on this below).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trade-off is real and I want to be upfront about it: &lt;strong&gt;if you change the source string, you've changed the key.&lt;/strong&gt; The old translation becomes orphaned. We'll come back to this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Runtime contract: no runtime package, just a CLI
&lt;/h2&gt;

&lt;p&gt;Here's a decision I spent a while on. I did &lt;strong&gt;not&lt;/strong&gt; publish a React or Vue runtime. I only ship a CLI.&lt;/p&gt;

&lt;p&gt;Reason: runtimes are a trap. Vite vs Webpack vs SSR vs micro-frontends, different i18next versions, different vue-i18n configs. If I wrap a "universal" runtime around all that, I'll either cover 60% of cases or fight with every framework's existing i18n setup.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;i18n-trans&lt;/code&gt; constrains exactly two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The shape of the locale JSON files.&lt;/li&gt;
&lt;li&gt;The contract for &lt;code&gt;t()&lt;/code&gt; — its signature and the lookup order.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Translate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;__TRANS_ID&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="na"&gt;param&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lookup order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;target locale: &amp;lt;source&amp;gt;__TRANS_ID_&amp;lt;id&amp;gt;
  → target locale: &amp;lt;source&amp;gt;
  → source locale: &amp;lt;source&amp;gt;
  → the source string itself
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the active language &lt;em&gt;is&lt;/em&gt; the source language, just return the source string.&lt;/p&gt;

&lt;p&gt;You can implement this on top of i18next, vue-i18n, or a 30-line hand-rolled function. Doesn't matter. As long as the lookup order matches, the CLI works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three commands
&lt;/h2&gt;

&lt;p&gt;That's the whole surface area:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;i18n-trans check                              &lt;span class="c"&gt;# audit source and target locales&lt;/span&gt;
i18n-trans check &lt;span class="nt"&gt;--add&lt;/span&gt; &lt;span class="nt"&gt;--add-target&lt;/span&gt; &lt;span class="nt"&gt;--remove&lt;/span&gt;  &lt;span class="c"&gt;# auto-fix&lt;/span&gt;
i18n-trans trans                             &lt;span class="c"&gt;# LLM translation pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;check&lt;/code&gt;: static scanning + diff
&lt;/h3&gt;

&lt;p&gt;The CLI uses Babel to statically find &lt;code&gt;t()&lt;/code&gt; / &lt;code&gt;translate()&lt;/code&gt; calls in your source. It supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TypeScript / TSX&lt;/li&gt;
&lt;li&gt;Vue SFC &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;script setup&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Vue templates — both &lt;code&gt;{{ t('Save') }}&lt;/code&gt; and bound attributes like &lt;code&gt;:title="t('Book', 'reserve')"&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No runtime needed, no dev server. It runs in CI the same way it runs locally.&lt;/p&gt;

&lt;p&gt;When you run &lt;code&gt;i18n-trans check&lt;/code&gt;, it reports four classes of issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source missing&lt;/strong&gt;: a &lt;code&gt;t('...')&lt;/code&gt; call whose key isn't in the source locale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source unused&lt;/strong&gt;: a key in the source locale that no code references.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target missing&lt;/strong&gt;: a source key with no translation in a target locale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target extra&lt;/strong&gt;: a key in a target locale that no longer exists in the source.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Flags make it self-healing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--add&lt;/code&gt;: add missing source keys with identity values.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--add-target&lt;/code&gt;: add missing target keys with a &lt;code&gt;⟦TODO⟧ &amp;lt;source&amp;gt;&lt;/code&gt; placeholder.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--remove&lt;/code&gt;: delete unused / orphaned keys.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a target locale file doesn't exist, it just creates one.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;trans&lt;/code&gt;: batch LLM translation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;i18n-trans trans
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It batches pending keys and posts them to any OpenAI-compatible endpoint (OpenAI, DeepSeek, local vLLM, whatever), defaulting to 20 units per batch, serially.&lt;/p&gt;

&lt;p&gt;Config lives in &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;I18N_LLM_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://api.openai.com/v1
&lt;span class="nv"&gt;I18N_LLM_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-...
&lt;span class="nv"&gt;I18N_LLM_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gpt-4o-mini
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A full demo
&lt;/h2&gt;

&lt;p&gt;Let's run the whole workflow on a tiny project. Here's &lt;code&gt;src/App.tsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;greeting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;save&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Save&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reserve&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Book&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reserve&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Initial state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;src/locales/en-US.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Hello"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hello"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;src/locales/es-ES.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Hello"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hola"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Legacy copy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Copia antigua"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I've deliberately planted three problems: the source locale is missing &lt;code&gt;Save&lt;/code&gt; and &lt;code&gt;Book&lt;/code&gt;, the target is missing their translations (plus the &lt;code&gt;reserve&lt;/code&gt; variant), and the target has an orphaned &lt;code&gt;Legacy copy&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 — check
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;i18n-trans check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;i18n check · 1 files scanned · 3 static t() keys

en-US (source) · src/locales/en-US.json
  ✖ missing 2 key(s)
    - Save  (src/App.tsx:2)
    - Book  (src/App.tsx:3)

es-ES (target) · src/locales/es-ES.json
  ✖ missing 1 translation(s)
    - Book__TRANS_ID_reserve
  ⚠ extra 1 key(s) (not in the source locale)
    - Legacy copy

✖ check failed: missing keys remain
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things to notice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Missing keys come with a &lt;code&gt;file:line&lt;/code&gt; so your editor can jump straight to them.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Book__TRANS_ID_reserve&lt;/code&gt; is the variant key the scanner derived from &lt;code&gt;t('Book', 'reserve')&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Legacy copy&lt;/code&gt; is flagged as &lt;em&gt;extra&lt;/em&gt; because it no longer exists in the source.&lt;/li&gt;
&lt;li&gt;Exit code is &lt;code&gt;1&lt;/code&gt; when anything is wrong, so this drops into a CI gate.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2 — fix
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;i18n-trans check &lt;span class="nt"&gt;--add&lt;/span&gt; &lt;span class="nt"&gt;--add-target&lt;/span&gt; &lt;span class="nt"&gt;--remove&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI first prints the audit, then the mutations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+ en-US: added 2 key(s) in src/locales/en-US.json
+ es-ES: added 3 key(s) in src/locales/es-ES.json
- es-ES: removed 1 key(s) in src/locales/es-ES.json

✔ check passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Diff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;// src/locales/en-US.json
 {
   "Hello": "Hello",
&lt;span class="gi"&gt;+  "Save": "Save",
+  "Book": "Book"
&lt;/span&gt; }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;// src/locales/es-ES.json
 {
   "Hello": "Hola",
&lt;span class="gd"&gt;-  "Legacy copy": "Copia antigua"
&lt;/span&gt;&lt;span class="gi"&gt;+  "Save": "⟦TODO⟧ Save",
+  "Book": "⟦TODO⟧ Book",
+  "Book__TRANS_ID_reserve": "⟦TODO⟧ Book"
&lt;/span&gt; }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;⟦TODO⟧&lt;/code&gt; means "key exists, no translation yet". It's a placeholder, not a translation. Running &lt;code&gt;check&lt;/code&gt; again now passes clean — but the target locale still has no real translations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3 — translate
&lt;/h3&gt;

&lt;p&gt;First a dry run to see what would be sent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;i18n-trans trans &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
i18n trans · en-US → es-ES
dry run: 3 unit&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt; to translate
  - Save
  - Book
  - Book__TRANS_ID_reserve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Hello&lt;/code&gt; already has a translation (&lt;code&gt;Hola&lt;/code&gt;), so it's skipped. Real run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;i18n-trans trans
i18n trans · en-US → es-ES
&lt;span class="o"&gt;[&lt;/span&gt;es-ES] 3 unit&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;es-ES]   ✔ 3 translated
translated 3, failed 0, skipped 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Hello"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hola"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Save"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"⟦REVIEW⟧ Guardar"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Book"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"⟦REVIEW⟧ Libro"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Book__TRANS_ID_reserve"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"⟦REVIEW⟧ Reservar"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;New translations are written with a &lt;code&gt;⟦REVIEW⟧&lt;/code&gt; prefix. That's deliberate — more on it below. The &lt;code&gt;skipped 1&lt;/code&gt; is the already-translated &lt;code&gt;Hello&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually goes to the LLM
&lt;/h2&gt;

&lt;p&gt;The CLI doesn't dump the entire locale file at the model. It builds per-unit translation requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;scan t() calls
  → derive expected keys (source key + __TRANS_ID variants)
  → load target locale JSON
  → find untranslated units
  → build unit { key, source, transId, context, hint }
  → batch by batchSize
  → POST /chat/completions
  → parse result
  → validate keys, non-empty, placeholders
  → write back as ⟦REVIEW⟧ translations
  → re-run check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What counts as "untranslated"
&lt;/h3&gt;

&lt;p&gt;A key is sent to the model if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's missing from the target locale, &lt;strong&gt;or&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Its value is a &lt;code&gt;⟦TODO⟧ &amp;lt;source&amp;gt;&lt;/code&gt; placeholder, &lt;strong&gt;or&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Its value equals the source string (handles legacy data).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's skipped if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It already has a clean translation,&lt;/li&gt;
&lt;li&gt;It's already marked &lt;code&gt;⟦REVIEW⟧&lt;/code&gt; (so you don't re-translate things you're about to review),&lt;/li&gt;
&lt;li&gt;It's listed in &lt;code&gt;translation.ignoreKeys&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The unit shape
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;TranslationUnit&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;           &lt;span class="c1"&gt;// "Book__TRANS_ID_reserve"&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;        &lt;span class="c1"&gt;// "Book"&lt;/span&gt;
  &lt;span class="nx"&gt;transId&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;     &lt;span class="c1"&gt;// "reserve"&lt;/span&gt;
  &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;     &lt;span class="c1"&gt;// surrounding code at the call site&lt;/span&gt;
  &lt;span class="nx"&gt;hint&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;         &lt;span class="c1"&gt;// manual note from i18n.config.json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;context&lt;/code&gt; is auto-extracted from a few lines around the &lt;code&gt;t()&lt;/code&gt; call. &lt;code&gt;hint&lt;/code&gt; is a manual override you can set per key.&lt;/p&gt;

&lt;h3&gt;
  
  
  The system prompt
&lt;/h3&gt;

&lt;p&gt;The system message pins down the contract:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Translate &lt;code&gt;source&lt;/code&gt;, not &lt;code&gt;key&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Return &lt;code&gt;{"results":[{"key":"...","value":"..."}]}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Keys must match input exactly — no additions, no omissions.&lt;/li&gt;
&lt;li&gt;Preserve &lt;code&gt;{name}&lt;/code&gt; and &lt;code&gt;{{name}}&lt;/code&gt; interpolation tokens, including brace style.&lt;/li&gt;
&lt;li&gt;Preserve HTML tags, numbers, product names, code identifiers.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;context&lt;/code&gt; and &lt;code&gt;hint&lt;/code&gt; to disambiguate.&lt;/li&gt;
&lt;li&gt;JSON only — no markdown fences, no commentary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Expected response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"results"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Save"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Guardar"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Book"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Libro"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Book__TRANS_ID_reserve"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Reservar"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI also tolerates bare arrays or flat key/value maps, but &lt;code&gt;results&lt;/code&gt; is the recommended shape.&lt;/p&gt;

&lt;h3&gt;
  
  
  Validation before writing anything
&lt;/h3&gt;

&lt;p&gt;Three checks happen before a translation is written to disk:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The returned key set matches the requested set exactly.&lt;/li&gt;
&lt;li&gt;Every value is a non-empty string.&lt;/li&gt;
&lt;li&gt;Interpolation tokens match between source and translation — same brace style, same count.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Any failure triggers a retry up to &lt;code&gt;maxRetries&lt;/code&gt;. If it still fails, the batch is logged and &lt;strong&gt;nothing is written&lt;/strong&gt;. Bad data never reaches the locale file.&lt;/p&gt;

&lt;p&gt;Default &lt;code&gt;batchSize&lt;/code&gt; is 20, runs serially. Smaller batches with context give more reliable output than one giant JSON blob, and failures are cheap to retry.&lt;/p&gt;

&lt;h2&gt;
  
  
  One word, two translations: &lt;code&gt;__TRANS_ID&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;"Book" is the classic example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A book you're reading → &lt;code&gt;Libro&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Book a flight or a restaurant table → &lt;code&gt;Reservar&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With semantic keys you'd invent two separate keys. Here you just tag the call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Book&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                &lt;span class="c1"&gt;// noun&lt;/span&gt;
&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Book&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;__TRANS_ID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reserve&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;     &lt;span class="c1"&gt;// verb&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Target file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Book"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Libro"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Book__TRANS_ID_reserve"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Reservar"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scanner picks up the second argument and derives the variant key automatically.&lt;/p&gt;

&lt;p&gt;The really useful part for LLMs is that you can attach a manual hint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"translation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"hints"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Book__TRANS_ID_reserve"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"This is the verb 'to book' — reserving a flight, hotel, or table. Not the noun 'a book you read'."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the model has a fighting chance to output "Reservar" instead of blindly copying "Libro".&lt;/p&gt;

&lt;h2&gt;
  
  
  The review markers you can't skip
&lt;/h2&gt;

&lt;p&gt;LLM translations look great until they don't. Two markers make the human review step impossible to forget:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Marker&lt;/th&gt;
&lt;th&gt;Written by&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;⟦TODO⟧ &amp;lt;source&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;check --add-target&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Key exists, no translation yet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;⟦REVIEW⟧ &amp;lt;translation&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;trans&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;AI translated, awaiting human review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;no marker&lt;/td&gt;
&lt;td&gt;human / &lt;code&gt;trans --ignore&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Confirmed translation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;trans&lt;/code&gt; writes &lt;code&gt;⟦REVIEW⟧&lt;/code&gt; by default, not clean translations. Search your project for &lt;code&gt;⟦REVIEW⟧&lt;/code&gt;, go through each one, delete the marker when you're happy.&lt;/p&gt;

&lt;p&gt;What this protects you from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Already-reviewed translations don't get re-translated.&lt;/li&gt;
&lt;li&gt;Existing translations are never overwritten.&lt;/li&gt;
&lt;li&gt;Interpolation tokens can't be silently dropped (validation catches it).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you trust a batch and want clean output immediately, run &lt;code&gt;trans --ignore&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Interpolation: &lt;code&gt;{name}&lt;/code&gt; and &lt;code&gt;{{name}}&lt;/code&gt; both work
&lt;/h2&gt;

&lt;p&gt;i18next uses &lt;code&gt;{{name}}&lt;/code&gt;. vue-i18n uses &lt;code&gt;{name}&lt;/code&gt;. The CLI detects which style the source uses and requires the translation to keep it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Hello, {name}!"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"¡Hola, {name}!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Welcome {{user}}!"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bienvenido, {{user}}!"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the model rewrites &lt;code&gt;{{name}}&lt;/code&gt; as &lt;code&gt;{name}&lt;/code&gt;, validation fails and the batch retries. No more broken interpolation in production.&lt;/p&gt;

&lt;p&gt;Runtime-side, you'll usually want to align the syntax:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// i18next: match vue-i18n's single-brace style&lt;/span&gt;
&lt;span class="nx"&gt;i18n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;fallbackLng&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en-US&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;keySeparator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;nsSeparator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;interpolation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;suffix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vue-i18n: source keys may contain . or /, so use a messageResolver&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;i18n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createI18n&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;legacy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messageResolver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt; &lt;span class="kd"&gt;as &lt;/span&gt;&lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Quick start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add &lt;span class="nt"&gt;-D&lt;/span&gt; @10coding/i18n-trans
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;i18n.config.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"include"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"src/**/*.{ts,tsx,vue}"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"localesDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"src/locales"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sourceLocale"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"en-US"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"targetLocales"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"es-ES"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"callers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"t"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"translate"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"transId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"option"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"__TRANS_ID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"separator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"__TRANS_ID_"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source locale:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;src/locales/en-US.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Hello, {name}!"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hello, {name}!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Book"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Book"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;LLM config (only needed for &lt;code&gt;trans&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .env&lt;/span&gt;
&lt;span class="nv"&gt;I18N_LLM_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://api.openai.com/v1
&lt;span class="nv"&gt;I18N_LLM_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-...
&lt;span class="nv"&gt;I18N_LLM_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gpt-4o-mini
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Optional knobs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;I18N_LLM_TIMEOUT_MS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;60000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Per-request timeout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;I18N_LLM_MAX_RETRIES&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Retries per failed batch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;I18N_LLM_JSON_MODE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Requests &lt;code&gt;response_format: json_object&lt;/code&gt;; set &lt;code&gt;false&lt;/code&gt; if your provider doesn't support it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;i18n-trans check
i18n-trans check &lt;span class="nt"&gt;--add&lt;/span&gt; &lt;span class="nt"&gt;--add-target&lt;/span&gt;
i18n-trans trans
i18n-trans check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Who this is for (and who it isn't)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;This clicks if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your engineers write the UI copy themselves in whatever source language you've chosen, and they also maintain the translations.&lt;/li&gt;
&lt;li&gt;You have enough UI copy that missing / dead keys are already a recurring annoyance.&lt;/li&gt;
&lt;li&gt;You want LLMs doing draft translations, but you still want a human review gate.&lt;/li&gt;
&lt;li&gt;You're juggling multiple frameworks or repos and want one shared workflow.&lt;/li&gt;
&lt;li&gt;You accept that editing the source string means the old translation becomes stale.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;This probably isn't for you if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have a dedicated localization team that needs to work from a neutral key catalog, away from your source code.&lt;/li&gt;
&lt;li&gt;You need complex ICU pluralization, gender, or select rules — the contract here is deliberately simple.&lt;/li&gt;
&lt;li&gt;Your source copy churns constantly and you can't afford translation refresh work.&lt;/li&gt;
&lt;li&gt;You've already paid for a mature commercial i18n platform and don't need LLM integration.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The trade-offs, honestly
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why not semantic keys?&lt;/strong&gt; They're not wrong — they're optimized for a different org structure: copy written outside the codebase, translated by a separate team. When the same engineers write the copy &lt;em&gt;and&lt;/em&gt; the translations, the source string is already the best context an LLM can get. The indirection costs more than it saves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why no runtime package?&lt;/strong&gt; Because every framework's i18n setup is a snowflake. The CLI constrains data shape and lookup order; you bring your own &lt;code&gt;t()&lt;/code&gt; on top of i18next, vue-i18n, or a hand-rolled 30-line function. The repo ships reference implementations for React and Vue, but they're examples, not published packages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why keep the review markers?&lt;/strong&gt; LLM output is good but not shippable-good. &lt;code&gt;⟦REVIEW⟧&lt;/code&gt; turns "AI translated it" and "a human signed off" into two distinct, greppable states.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;The idea isn't "yet another i18n library". It's splitting i18n into three concerns:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Data format&lt;/strong&gt;: source string as key, flat JSON.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime contract&lt;/strong&gt;: a known &lt;code&gt;t()&lt;/code&gt; lookup order plus &lt;code&gt;__TRANS_ID&lt;/code&gt; variants.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workflow&lt;/strong&gt;: CLI scans, fills, and translates; humans review.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your existing framework runtime stays in place. LLMs do the bulk translation. Markers and validation hold the quality line.&lt;/p&gt;

&lt;p&gt;Links:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/ALiuYiLin/i18n-llm-trans" rel="noopener noreferrer"&gt;ALiuYiLin/i18n-llm-trans&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/@10coding/i18n-trans" rel="noopener noreferrer"&gt;@10coding/i18n-trans&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Contract: &lt;a href="https://github.com/ALiuYiLin/i18n-llm-trans/blob/master/docs/i18n-contract.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/i18n-contract.md&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're already doing source-string-as-keys and hand-rolling the plumbing, I'd love to hear what you've built. Star the repo if this saves you a few hours.&lt;/p&gt;




&lt;p&gt;Tags: &lt;code&gt;#webdev&lt;/code&gt; &lt;code&gt;#typescript&lt;/code&gt; &lt;code&gt;#react&lt;/code&gt; &lt;code&gt;#vue&lt;/code&gt; &lt;code&gt;#opensource&lt;/code&gt; &lt;code&gt;#i18n&lt;/code&gt; &lt;code&gt;#llm&lt;/code&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>frontend</category>
      <category>javascript</category>
      <category>llm</category>
    </item>
  </channel>
</rss>
