<?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: Franco Ortiz</title>
    <description>The latest articles on DEV Community by Franco Ortiz (@pakvothe).</description>
    <link>https://dev.to/pakvothe</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3851680%2Fad99bd9b-108e-43a4-90d8-dc9a2c8b2bc4.jpg</url>
      <title>DEV Community: Franco Ortiz</title>
      <link>https://dev.to/pakvothe</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pakvothe"/>
    <language>en</language>
    <item>
      <title>I mass-translated 200 keys into 5 languages. It went wrong in ways I didn't expect.</title>
      <dc:creator>Franco Ortiz</dc:creator>
      <pubDate>Tue, 07 Apr 2026 19:36:29 +0000</pubDate>
      <link>https://dev.to/pakvothe/i-mass-translated-200-keys-into-5-languages-it-went-wrong-in-ways-i-didnt-expect-1nfe</link>
      <guid>https://dev.to/pakvothe/i-mass-translated-200-keys-into-5-languages-it-went-wrong-in-ways-i-didnt-expect-1nfe</guid>
      <description>&lt;p&gt;Last project I had to internationalize an app with around 200 keys across 5 languages. I figured it would take a morning. It took most of a day, and the interesting part wasn't the translating. It was everything else.&lt;/p&gt;

&lt;p&gt;The actual translations were fine. DeepL, Google Translate, whatever. Ten minutes. What killed me was the workflow around it:&lt;/p&gt;

&lt;p&gt;Creating 5 copies of every JSON file. Keeping them in sync. Discovering that &lt;code&gt;{name}&lt;/code&gt; got translated to &lt;code&gt;{nombre}&lt;/code&gt; in the Spanish file and broke interpolation. Finding a typo (&lt;code&gt;settigns.title&lt;/code&gt;) that rendered as blank in production because there's no runtime error for a missing key. Realizing a week later that I forgot three keys in Japanese entirely.&lt;/p&gt;

&lt;p&gt;The thing that surprised me was the ratio. The translation itself was maybe 10% of the time. The other 90% was file management, variable protection, and chasing silent failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  What silent failures actually look like
&lt;/h2&gt;

&lt;p&gt;This is the part nobody talks about. Your app doesn't crash when a translation key is wrong. It just renders nothing. Or it renders &lt;code&gt;common.header.subtitl&lt;/code&gt; as literal text. Or it works in English and Spanish but breaks in Japanese because you have &lt;code&gt;{count} items&lt;/code&gt; and the variable got mangled.&lt;/p&gt;

&lt;p&gt;You don't catch these in development because you're developing in English. They show up in production, reported by users who don't speak English, often weeks later.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I stopped doing it manually
&lt;/h2&gt;

&lt;p&gt;I built &lt;a href="https://github.com/Pakvothe/i1n-cli" rel="noopener noreferrer"&gt;i1n&lt;/a&gt; to automate the parts that kept breaking. The whole workflow now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;i1n push &lt;span class="nt"&gt;--translate&lt;/span&gt; es,fr,de,ja,pt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One command. Variables stay untouched in every language. And it generates a &lt;code&gt;.d.ts&lt;/code&gt; file, so if I typo a key, TypeScript catches it before I even run the app.&lt;/p&gt;

&lt;p&gt;But the real shift was adding an MCP server. If you use Cursor or Claude Code, you can tell your agent "internationalize this component" and it extracts the strings, translates, and rewrites the code. I was skeptical about this one. For repetitive extraction work, it's genuinely good.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 85% problem
&lt;/h2&gt;

&lt;p&gt;AI translation isn't perfect though. It gets context wrong on short strings ("Save" as a verb vs "Save" as a noun), goes too literal on idioms, and sometimes loses tone completely. I've seen it translate "Drop us a line" as the equivalent of "drop a rope" in Japanese.&lt;/p&gt;

&lt;p&gt;That's why I ended up building a dashboard too. The AI handles the first pass, you review what it generated, fix what needs fixing, and mark those as approved so they don't get overwritten next time. If your source text changes later, the system flags which translations went stale.&lt;/p&gt;

&lt;p&gt;85% automation, 15% human review. That split turned a full day of work into maybe 20 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell someone setting up i18n today
&lt;/h2&gt;

&lt;p&gt;Don't start with the translation. Start with the workflow. Figure out how you're going to keep files in sync, how you'll catch missing keys before production, and how you'll handle variables across languages. The translation is the easy part.&lt;/p&gt;

&lt;p&gt;The CLI and dashboard are open source (MIT) if you want to try it: &lt;a href="https://i1n.ai" rel="noopener noreferrer"&gt;i1n.ai&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How are you handling translations? I keep running into teams that either set up a full TMS they barely use, or just skip internationalization entirely because the workflow is too painful.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>javascript</category>
      <category>productivity</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I told my AI agent "internationalize this" and it actually did it</title>
      <dc:creator>Franco Ortiz</dc:creator>
      <pubDate>Tue, 31 Mar 2026 13:37:36 +0000</pubDate>
      <link>https://dev.to/pakvothe/i-told-my-ai-agent-internationalize-this-and-it-actually-did-it-45on</link>
      <guid>https://dev.to/pakvothe/i-told-my-ai-agent-internationalize-this-and-it-actually-did-it-45on</guid>
      <description>&lt;p&gt;I've set up i18n in Next.js App Router projects more times than I'd like to admit. It's always the same: middleware config, a pile of JSON files, wiring up the provider, copy-pasting keys between locales, discovering at 11pm that &lt;code&gt;common.heor.title&lt;/code&gt; returns undefined in production.&lt;/p&gt;

&lt;p&gt;The last time it happened I was mass-translating 40+ keys across 5 languages by hand. I stopped halfway through and thought: why am I still doing this manually?&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://i1n.ai/en" rel="noopener noreferrer"&gt;i1n&lt;/a&gt;, an open source CLI (MIT) that pushes your translation keys and gets them back translated with full TypeScript types. Free tier, no credit card, no 14-day trial tricks.&lt;/p&gt;

&lt;p&gt;Let me walk you through the setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A Next.js project (App Router or Pages Router)&lt;/li&gt;
&lt;li&gt;Node.js 18+&lt;/li&gt;
&lt;li&gt;~5 minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Install and init
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; i1n
i1n init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It'll ask for an API key (you can grab one at &lt;a href="https://dashboard.i1n.ai/login" rel="noopener noreferrer"&gt;i1n.ai&lt;/a&gt;, Google or GitHub login). After that it detects your framework and sets up the locale directory automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Write your source strings
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;locales/en_us/common.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;"hero"&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;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Welcome to our app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"subtitle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The best tool for {task}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"cta"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Get Started"&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;"nav"&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;"home"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Home"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"about"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"About"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"pricing"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Pricing"&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;Nothing new here — same JSON structure you're probably used to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Push and translate
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;i1n push &lt;span class="nt"&gt;--translate&lt;/span&gt; es,fr,de,ja
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One command. It pushes your keys, translates to 4 languages, and generates &lt;code&gt;i1n.d.ts&lt;/code&gt; with autocomplete for every key. Variables like &lt;code&gt;{task}&lt;/code&gt; are left untouched in every language.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Use it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;i1n&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;function&lt;/span&gt; &lt;span class="nf"&gt;Hero&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&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;hero.title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&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;hero.subtitle&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;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;building products&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&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;hero.cta&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;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;p&gt;If you typo a key, TypeScript yells at you before it ever hits production. No more &lt;code&gt;t('hera.titl')&lt;/code&gt; silently rendering nothing.&lt;/p&gt;

&lt;p&gt;If you're already on i18next or next-intl, there's a Bridge Mode that wraps your existing setup with type safety. One line, no migration. &lt;a href="https://i1n.ai/en/docs/bridge-mode/" rel="noopener noreferrer"&gt;Details in the docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: MCP Server
&lt;/h2&gt;

&lt;p&gt;This part is honestly the most fun. If you use Cursor, Claude Code, or Windsurf:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add i1n &lt;span class="nt"&gt;--&lt;/span&gt; npx i1n mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can just tell your AI agent &lt;em&gt;"internationalize this component"&lt;/em&gt; and it actually does it. Reads the file, extracts the strings, pushes them, translates everything, rewrites the code with &lt;code&gt;t()&lt;/code&gt; calls. What used to take me an hour is like 30 seconds now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What does your i18n setup look like?&lt;/strong&gt; I've been going back and forth between next-intl and i18next with the App Router and I'm curious what other people landed on.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>ai</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
