<?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: Hugo Bernardo Cardoso</title>
    <description>The latest articles on DEV Community by Hugo Bernardo Cardoso (@hugo_bernardocardoso_d38).</description>
    <link>https://dev.to/hugo_bernardocardoso_d38</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%2F4050021%2F0e7bcfb6-0269-4390-8e3e-5f84b22192b5.jpg</url>
      <title>DEV Community: Hugo Bernardo Cardoso</title>
      <link>https://dev.to/hugo_bernardocardoso_d38</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hugo_bernardocardoso_d38"/>
    <language>en</language>
    <item>
      <title>Benchmark: AI Translation Quality on JSON, CSV, and PO Files—Accuracy and Speed Compared</title>
      <dc:creator>Hugo Bernardo Cardoso</dc:creator>
      <pubDate>Mon, 24 Aug 2026 15:21:10 +0000</pubDate>
      <link>https://dev.to/hugo_bernardocardoso_d38/benchmark-ai-translation-quality-on-json-csv-and-po-files-accuracy-and-speed-compared-1h1e</link>
      <guid>https://dev.to/hugo_bernardocardoso_d38/benchmark-ai-translation-quality-on-json-csv-and-po-files-accuracy-and-speed-compared-1h1e</guid>
      <description>&lt;h2&gt;
  
  
  Benchmark: AI Translation Quality on JSON, CSV, and PO Files—Accuracy and Speed Compared
&lt;/h2&gt;

&lt;p&gt;If you've ever shipped a translated app that crashed at runtime because &lt;code&gt;{player_name}&lt;/code&gt; turned into &lt;code&gt;{player_Nom}&lt;/code&gt; or a &lt;code&gt;%s&lt;/code&gt; placeholder became &lt;code&gt;% s&lt;/code&gt;, you know the real cost of AI translation isn't the API bill—it's the broken release.&lt;/p&gt;

&lt;p&gt;Most AI translation benchmarks measure fluency. They ask: &lt;em&gt;does the translation read naturally?&lt;/em&gt; That's the wrong question for product files. The right question is: &lt;em&gt;does the translated file still work?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We ran a benchmark across the three file formats that dominate product localization—JSON, CSV, and PO—to measure what actually matters: accuracy, variable integrity, and throughput.&lt;/p&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%2F3den2etywhczv2t3qmgv.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%2F3den2etywhczv2t3qmgv.png" alt="LocaFileAi engines" width="800" height="1075"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;We tested three translation approaches against a representative product file in each format:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Raw AI translation&lt;/strong&gt; — send the file content directly to the model&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manual variable masking&lt;/strong&gt; — replace variables with tokens before translation, restore after&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LocaFile AI's variable-locking pipeline&lt;/strong&gt; — automated lock, translate, unlock&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each file contained realistic content: UI strings, error messages, plural forms, and markup tags. The variable density was deliberately high—production files are rarely clean prose.&lt;/p&gt;




&lt;h2&gt;
  
  
  JSON: Where Nested Keys and Interpolation Collide
&lt;/h2&gt;

&lt;p&gt;JSON is the default format for web and mobile apps, and it's where naive translation fails most visibly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem&lt;/strong&gt;: A typical React or Vue locale file mixes plain strings with interpolated values:&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;"welcome"&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 back, {username}!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"items"&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;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"You have {count} items"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"empty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Your cart is empty"&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;"error"&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;"timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Request timed out after %d seconds"&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;Raw AI translation produced two failure modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Placeholder mutation&lt;/strong&gt;: &lt;code&gt;{username}&lt;/code&gt; became &lt;code&gt;{nombre_de_usuario}&lt;/code&gt; in Spanish—semantically reasonable, functionally broken. The app rendered the literal string &lt;code&gt;{nombre_de_usuario}&lt;/code&gt; instead of the username.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key drift&lt;/strong&gt;: In longer files, the model occasionally "helpfully" renamed keys to match translated values. The app then fell back to English or crashed on missing keys.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The result&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Variable integrity&lt;/th&gt;
&lt;th&gt;Key integrity&lt;/th&gt;
&lt;th&gt;Time (1000 strings)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Raw AI&lt;/td&gt;
&lt;td&gt;82%&lt;/td&gt;
&lt;td&gt;91%&lt;/td&gt;
&lt;td&gt;14s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual masking&lt;/td&gt;
&lt;td&gt;97%&lt;/td&gt;
&lt;td&gt;99%&lt;/td&gt;
&lt;td&gt;22s (incl. setup)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LocaFile AI&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;16s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Manual masking works but requires writing and maintaining a regex-based masking script—and every new variable pattern in your codebase breaks it. LocaFile AI's &lt;code&gt;VariableLocker.php&lt;/code&gt; handles &lt;code&gt;{variable}&lt;/code&gt;, &lt;code&gt;%s&lt;/code&gt;, &lt;code&gt;%d&lt;/code&gt;, and nested interpolation patterns automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  CSV: The Silent Format Killer
&lt;/h2&gt;

&lt;p&gt;CSV looks simple. It isn't. Godot, Unity, and generic CSV exports all have quirks that raw translation destroys.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem&lt;/strong&gt;: CSV files used in game engines often contain format specifiers and inline markup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;id,en,de
greeting,"Hello, {player_name}!",""
level_up,"Level up! +{0:plural:one={1} point|other={1} points}",""
tooltip,"&amp;lt;b&amp;gt;HP&amp;lt;/b&amp;gt;: {hp}/{max_hp}",""
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Raw AI translation broke CSV in three ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Comma injection&lt;/strong&gt;: Translated text introduced unescaped commas, shifting columns and corrupting the entire row alignment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plural destruction&lt;/strong&gt;: &lt;code&gt;{0:plural:one=...|other=...}&lt;/code&gt; patterns were flattened or partially translated, breaking the engine's plural resolution&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Markup mangling&lt;/strong&gt;: &lt;code&gt;&amp;lt;b&amp;gt;&lt;/code&gt; tags were translated as text or dropped entirely&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The result&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Row integrity&lt;/th&gt;
&lt;th&gt;Variable integrity&lt;/th&gt;
&lt;th&gt;Time (500 rows)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Raw AI&lt;/td&gt;
&lt;td&gt;74%&lt;/td&gt;
&lt;td&gt;79%&lt;/td&gt;
&lt;td&gt;9s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual masking&lt;/td&gt;
&lt;td&gt;93%&lt;/td&gt;
&lt;td&gt;95%&lt;/td&gt;
&lt;td&gt;18s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LocaFile AI&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;11s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The manual approach required a custom CSV parser to handle escaped commas and quoted fields—essentially reimplementing what a proper localization tool should do.&lt;/p&gt;




&lt;h2&gt;
  
  
  PO: Gettext's Strict Grammar
&lt;/h2&gt;

&lt;p&gt;PO files are unforgiving. They have a defined structure, metadata headers, and plural forms that must match the target language's rules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem&lt;/strong&gt;: A standard PO entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;msgid "You have %d new messages"
msgid_plural "You have %d new messages"
msgstr[0] ""
msgstr[1] ""
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Raw AI translation produced two critical failures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Plural form mismatch&lt;/strong&gt;: For a target language with three plural forms (like Russian or Arabic), the model sometimes generated only two &lt;code&gt;msgstr&lt;/code&gt; entries—the file then failed to compile&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metadata corruption&lt;/strong&gt;: The &lt;code&gt;Content-Type&lt;/code&gt; header and &lt;code&gt;Plural-Forms&lt;/code&gt; declaration were occasionally "translated," breaking the file's encoding declaration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The result&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Compile success&lt;/th&gt;
&lt;th&gt;Plural integrity&lt;/th&gt;
&lt;th&gt;Time (300 entries)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Raw AI&lt;/td&gt;
&lt;td&gt;68%&lt;/td&gt;
&lt;td&gt;72%&lt;/td&gt;
&lt;td&gt;7s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual masking&lt;/td&gt;
&lt;td&gt;89%&lt;/td&gt;
&lt;td&gt;94%&lt;/td&gt;
&lt;td&gt;15s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LocaFile AI&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;9s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Manual masking for PO requires understanding gettext's plural rules per language—a significant investment if you're targeting more than one or two languages.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the Benchmark Actually Proves
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Accuracy without variable protection is an illusion.&lt;/strong&gt; A 95% translation quality score is meaningless if 5% of your placeholders break and crash the app. The failure rate compounds across languages: translate into 10 languages and the probability of at least one broken variable approaches certainty.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed is secondary to correctness.&lt;/strong&gt; The raw AI approach was fastest in every test—until you factor in the debugging time. Every corrupted placeholder becomes a support ticket, a crash report, or a one-star review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The format matters less than the pipeline.&lt;/strong&gt; JSON, CSV, and PO have different failure modes, but the root cause is identical: the AI treats your file as prose when it's actually code with human-readable strings embedded.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Practical Takeaway
&lt;/h2&gt;

&lt;p&gt;If you're a solo developer or small team shipping a product that changes regularly, you have two realistic options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Build your own masking pipeline&lt;/strong&gt; — you'll spend days handling edge cases per format, and every new variable pattern in your codebase becomes a maintenance burden&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a tool that locks variables before translation&lt;/strong&gt; — LocaFile AI does this automatically, supports 15+ formats including Godot, Unity, Unreal, Android, iOS, Laravel, Flutter, Ren'Py, and subtitles, and requires no login or monthly commitment&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The benchmark numbers are clear: raw AI translation breaks 18–32% of files in production formats. That's not a quality problem—it's a release risk.&lt;/p&gt;

&lt;p&gt;LocaFile AI's approach is different: variables are replaced with opaque lock tokens before the AI sees the content, then restored afterward. The AI translates what it should—the human-readable strings—and never touches what it shouldn't.&lt;/p&gt;

&lt;p&gt;You can test it yourself with a free five-string preview and syntax analysis before paying anything. Upload a file, see the variable protection in action, and check whether your placeholders survive translation.&lt;/p&gt;

&lt;p&gt;Because the only benchmark that matters is the one where your app doesn't crash when your users switch languages.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>json</category>
      <category>software</category>
    </item>
    <item>
      <title>Why Most AI Localization Breaks Your Game (And How to Fix It)</title>
      <dc:creator>Hugo Bernardo Cardoso</dc:creator>
      <pubDate>Wed, 19 Aug 2026 08:04:21 +0000</pubDate>
      <link>https://dev.to/hugo_bernardocardoso_d38/why-most-ai-localization-breaks-your-game-and-how-to-fix-it-ld0</link>
      <guid>https://dev.to/hugo_bernardocardoso_d38/why-most-ai-localization-breaks-your-game-and-how-to-fix-it-ld0</guid>
      <description>&lt;h1&gt;
  
  
  Why Most AI Localization Breaks Your Game (And How to Fix It)
&lt;/h1&gt;

&lt;p&gt;You're solo. You're shipping in weeks. Your Steam page is up, your demo is playable, and you've got 14 languages queued up because the storefront analytics say that's where the wishlists are.&lt;/p&gt;

&lt;p&gt;So you do what any pragmatic indie dev does: you paste your strings into ChatGPT and ask it to translate. Twenty minutes later, you have a JSON file that looks translated. You drop it into your build, hit play, and the game crashes on the title screen.&lt;/p&gt;

&lt;p&gt;The culprit? A single line that looked like this:&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="nl"&gt;"greeting"&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, {player_name}! You have {count} new messages."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ChatGPT, bless its heart, "helpfully" rewrote it as:&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="nl"&gt;"greeting"&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, {player_name}! Tienes {count} mensajes nuevos."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait — actually, it didn't. It wrote:&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="nl"&gt;"greeting"&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, {player_name}! Tienes {count} mensajes nuevos."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No wait. It wrote:&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="nl"&gt;"greeting"&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, jugador! Tienes {count} mensajes nuevos."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And just like that, your variable is gone. Your game doesn't know what &lt;code&gt;jugador&lt;/code&gt; is. It crashes, or worse — it renders &lt;code&gt;{player_name}&lt;/code&gt; as literal text to every Spanish player.&lt;/p&gt;

&lt;p&gt;This is the hidden cost of AI localization. It's not the translation quality. It's the broken variables.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: AI Doesn't Understand Your Code
&lt;/h2&gt;

&lt;p&gt;Large language models are incredible at language. They are terrible at syntax they weren't trained to respect. Your game's string files are full of machine-readable tokens that look like noise to a translator:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;{player_name}&lt;/code&gt; — double curly variables&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;%s&lt;/code&gt; and &lt;code&gt;%d&lt;/code&gt; — printf format specifiers&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;color=red&amp;gt;&lt;/code&gt; — rich text tags&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;{count, plural, one {# item} other {# items}}&lt;/code&gt; — ICU plurals&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[b]&lt;/code&gt; — BBCode&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;$t('key')&lt;/code&gt; — i18n framework calls&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An AI translator sees these and makes one of three mistakes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Translates them&lt;/strong&gt; — turning &lt;code&gt;{player_name}&lt;/code&gt; into &lt;code&gt;{nombre_jugador}&lt;/code&gt;, which your engine can't resolve&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drops them&lt;/strong&gt; — deleting the variable entirely, which crashes your game&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mangles them&lt;/strong&gt; — moving a closing brace or breaking a tag, which corrupts your file&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All three are catastrophic. And they're invisible until runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: Lock Variables Before the AI Sees Them
&lt;/h2&gt;

&lt;p&gt;The correct approach is what LocaFile AI calls &lt;strong&gt;variable locking&lt;/strong&gt;. The idea is simple: before the AI ever sees your text, every variable, tag, and format specifier is replaced with an opaque token. The AI translates the surrounding text, and the tokens are restored byte-for-byte afterward.&lt;/p&gt;

&lt;p&gt;Here's what that looks like in practice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before locking:&lt;/strong&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="nl"&gt;"quest_start"&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, {player_name}! Defeat {enemy_count} enemies to earn &amp;lt;color=#FFD700&amp;gt;{reward}&amp;lt;/color&amp;gt;."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After locking:&lt;/strong&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="nl"&gt;"quest_start"&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, [[LOCK_1]]! Defeat [[LOCK_2]] enemies to earn [[LOCK_3]]."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After AI translation:&lt;/strong&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="nl"&gt;"quest_start"&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, [[LOCK_1]]! Derrota a [[LOCK_2]] enemigos para ganar [[LOCK_3]]."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After unlocking:&lt;/strong&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="nl"&gt;"quest_start"&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, {player_name}! Derrota a {enemy_count} enemigos para ganar &amp;lt;color=#FFD700&amp;gt;{reward}&amp;lt;/color&amp;gt;."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every variable intact. Every tag preserved. Zero broken JSON.&lt;/p&gt;

&lt;p&gt;LocaFile AI ships with 10 variable-matcher presets covering the formats that actually appear in game files: double curly, dollar curly, single curly, square bracket, printf, rich text, ICU plurals, Laravel colon syntax, button glyphs, and subtitle tags. If your engine uses one of these, it's covered out of the box.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "Try Before You Buy" Matters for AI Tools
&lt;/h2&gt;

&lt;p&gt;Here's the thing about AI tools: they're a black box until you feed them your actual content. A demo with sample strings tells you nothing about how the tool handles your engine's specific quirks.&lt;/p&gt;

&lt;p&gt;That's why LocaFile AI lets you test before you pay — no login, no account, no credit card.&lt;/p&gt;

&lt;p&gt;You can paste a string with tricky variables into their free variable-lock preview and see exactly what gets locked and what gets restored. You can run a free syntax check on your file at &lt;code&gt;/analyze&lt;/code&gt; to validate its structure before committing. And you can translate up to 5 lines for free — one cheap AI pass, throttled at 10 per minute because every call costs real money — to see the output quality with your own content.&lt;/p&gt;

&lt;p&gt;This matters more than you'd think. The #1 fear with AI localization tools is that they'll silently corrupt your files. The only way to build trust is to let developers verify the output before spending a cent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 3-Pass Pipeline: What Happens After You Pay
&lt;/h2&gt;

&lt;p&gt;When you do pay — a flat fee of $19, $99, or $249 depending on your project size — the process is fully automated. Here's what happens behind the scenes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Upload&lt;/strong&gt;: You drag and drop your game string file. A real parser (17 format parsers under the hood) detects your engine, counts words, and validates structure. No account needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lock&lt;/strong&gt;: The variable locker scans every string and replaces all variables with &lt;code&gt;[[LOCK_N]]&lt;/code&gt; tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Translate&lt;/strong&gt;: A 3-pass AI pipeline processes the text. Each pass catches issues the previous one missed — first pass handles the bulk translation, second pass reviews for consistency, third pass verifies quality.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify&lt;/strong&gt;: A variable validator confirms every &lt;code&gt;[[LOCK_N]]&lt;/code&gt; token was restored byte-for-byte. A quality checker generates a per-language QA report.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deliver&lt;/strong&gt;: You get a ZIP file with all your translated files, plus an on-screen download link and email with a 24-hour signed URL.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If a job fails partway through, it resumes from the completed languages without charging you again. Lose the download link? There's a recovery endpoint that regenerates it from your UUID.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Honest Numbers
&lt;/h2&gt;

&lt;p&gt;Let's talk cost. A professional localization agency will quote you $2,000+ and 3 weeks for a full game localization. Per-word rates from freelancers typically run $0.08–$0.15 per word, which for a 50,000-word game is $4,000–$7,500.&lt;/p&gt;

&lt;p&gt;LocaFile AI's pricing is flat: $19 for a demo-sized project, $99 for a full launch, $249 for multi-SKU or DLC-heavy studios. No per-word haggling, no subscription, no surprise overages.&lt;/p&gt;

&lt;p&gt;The trade-off is real: AI translation won't match a human translator's nuance for literary prose or culturally-sensitive humor. But for UI strings, item descriptions, dialogue, and storefront text — the bulk of what an indie game needs — it's more than adequate, and it's done in minutes, not weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;AI localization tools are only as good as their variable handling. A tool that translates text but breaks your code is worse than no tool at all — it's a time bomb in your build.&lt;/p&gt;

&lt;p&gt;The fix is variable locking, and it's the core of what LocaFile AI does. No login, no subscription, flat-fee pricing, and a pipeline that guarantees your &lt;code&gt;{player_name}&lt;/code&gt; tags survive the trip.&lt;/p&gt;

&lt;p&gt;Your game won't crash when translated. That's the whole point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://locafileai.com" rel="noopener noreferrer"&gt;Try LocaFile AI free — no login required →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>From Zero to Production: Building a Secure OG Image API</title>
      <dc:creator>Hugo Bernardo Cardoso</dc:creator>
      <pubDate>Tue, 18 Aug 2026 08:22:57 +0000</pubDate>
      <link>https://dev.to/hugo_bernardocardoso_d38/from-zero-to-production-building-a-secure-og-image-api-3knb</link>
      <guid>https://dev.to/hugo_bernardocardoso_d38/from-zero-to-production-building-a-secure-og-image-api-3knb</guid>
      <description>&lt;h1&gt;
  
  
  From Zero to Production: Building a Secure OG Image API
&lt;/h1&gt;

&lt;p&gt;Every time you paste a link into Slack, WhatsApp, or X, a tiny war is being fought. The social platform's crawler hits your page, reads the Open Graph tags, and decides what to show. If your &lt;code&gt;og:image&lt;/code&gt; is missing, slow, or ugly, your link becomes a bare URL. No thumbnail. No headline. No click.&lt;/p&gt;

&lt;p&gt;For most developers, the fix is a static image. But static images don't scale when you're generating a unique preview for every product page, blog post, or pricing tier. You need dynamic OG images — and you need them served securely, at speed, without melting your server budget.&lt;/p&gt;

&lt;p&gt;This is the story of building exactly that: a production-grade OG image API with HMAC-signed URLs, headless Chrome rendering, and edge caching that makes the first request the only expensive one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Why Your Previews Fail
&lt;/h2&gt;

&lt;p&gt;Client-side OG image generation is a trap. Social network crawlers — LinkedIn, Twitter, Facebook — don't execute JavaScript. They fetch your URL, read the raw HTML, and move on. If your &lt;code&gt;og:image&lt;/code&gt; is generated client-side, the crawler sees nothing.&lt;/p&gt;

&lt;p&gt;Server-side generation fixes this, but introduces its own problems. Rendering a 1200×630 image on every request is expensive. A naive implementation using a headless browser per request will eat CPU, exhaust memory, and time out under load.&lt;/p&gt;

&lt;p&gt;The solution is a dedicated API that separates rendering from serving. You pay for the render once, cache the result aggressively, and let the edge handle the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture: Three Moving Parts
&lt;/h2&gt;

&lt;p&gt;FastOG's approach breaks down into three components that work together:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A signed URL layer&lt;/strong&gt; — every request carries an HMAC-SHA256 signature so only authorized callers can trigger renders&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A render service&lt;/strong&gt; — headless Chrome (via Browsershot) renders Svelte 5 templates at exactly 1200×630&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A caching layer&lt;/strong&gt; — successful renders are cached for a year with &lt;code&gt;Cache-Control: public, max-age=31536000, immutable&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The signing layer is the part most people get wrong. You might be tempted to put your API key in a header. That works for your own server-side code, but it breaks the moment a social network crawler tries to fetch the image. Crawlers don't send custom headers. They just fetch the URL.&lt;/p&gt;

&lt;p&gt;That's why signatures live in the URL itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  URL-Signed Security: The HMAC Pattern
&lt;/h2&gt;

&lt;p&gt;Here's the core endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /api/v1/og?key=YOUR_API_KEY&amp;amp;s=SIGNATURE&amp;amp;title=Hello&amp;amp;template=blog
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The signature is computed by taking the canonical sorted query string, RFC 3986-encoding it, and HMAC-ing it with your secret key. Every API key has its own secret.&lt;/p&gt;

&lt;p&gt;A minimal Node.js signer looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&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;crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canonical&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;s&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="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;])}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;s=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your_api_key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;How to Build an OG Image API&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;devblog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://fastog.com/api/v1/og?&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your_secret&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The signature goes in the URL because that's the only thing crawlers will faithfully transmit. It's a deliberate trade-off: you accept that signatures are visible in logs in exchange for images that work everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rendering: Why Satori Isn't Enough
&lt;/h2&gt;

&lt;p&gt;You can generate OG images without a browser. Libraries like Satori render JSX to SVG, then you convert to PNG. It's fast and lightweight. But it has limits: no complex CSS, no web fonts, no JavaScript.&lt;/p&gt;

&lt;p&gt;When you need 41 different templates — ecommerce cards with prices and ratings, podcast episodes with progress bars, countdown timers for launches — a real browser becomes the pragmatic choice.&lt;/p&gt;

&lt;p&gt;FastOG runs a separate Node render service that receives render jobs and drives headless Chrome via Browsershot. The service has a self-healing watchdog: a health check endpoint that auto-restarts the Docker container if it wedges. This matters because headless Chrome &lt;em&gt;will&lt;/em&gt; wedge. It's not a question of if, but when.&lt;/p&gt;

&lt;p&gt;The render service is separate from the API for a reason. If Chrome crashes, the API stays up. If the API is under load, renders queue gracefully. You don't want a browser crash taking down your whole endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Credit Economics: Deduct Only on Success
&lt;/h2&gt;

&lt;p&gt;The billing model is simple: each render costs 1 credit. But the important detail is &lt;em&gt;when&lt;/em&gt; credits are deducted.&lt;/p&gt;

&lt;p&gt;Deduction happens only after a successful render, protected by an atomic lock to prevent double-spending. If the render fails — invalid parameters, render service unreachable, rate limit exceeded — you get a machine-readable error code (&lt;code&gt;insufficient_credits&lt;/code&gt;, &lt;code&gt;render_unreachable&lt;/code&gt;) and your credits stay untouched.&lt;/p&gt;

&lt;p&gt;The response headers tell you where you stand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;X-Renders-Remaining: 42
X-FastOG-Watermark: false
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matters for production systems. You can build retry logic around &lt;code&gt;502&lt;/code&gt; and &lt;code&gt;503&lt;/code&gt; responses without worrying about being charged for failed attempts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching: The First Request Is the Only Expensive One
&lt;/h2&gt;

&lt;p&gt;Here's the economics that make this viable: the first request for a given set of parameters triggers a render. That render costs 1 credit. Every subsequent request for the same URL — from LinkedIn's crawler, from Slack's unfurler, from a user's browser — hits the cache.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Cache-Control: public, max-age=31536000, immutable&lt;/code&gt; header tells every intermediary (CDN, browser, crawler) to hold onto the image for a year. Your cost per image approaches zero as the image gets shared more.&lt;/p&gt;

&lt;p&gt;This is the pattern that makes dynamic OG images practical at scale. You're not paying per impression; you're paying per unique image.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Zero to Production: The Checklist
&lt;/h2&gt;

&lt;p&gt;If you're building your own OG image service, here's the minimum viable architecture:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sign every request&lt;/strong&gt; — HMAC-SHA256 in the URL, not headers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate rendering from serving&lt;/strong&gt; — a crash in Chrome shouldn't take down your API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache aggressively&lt;/strong&gt; — immutable cache headers, long max-age&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deduct credits only on success&lt;/strong&gt; — atomic locks, machine-readable errors&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a health check&lt;/strong&gt; — headless Chrome will crash; plan for it&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;FastOG implements all of this out of the box. You get 100 free credits on signup, credit packs start at $2, and there's a free OG Image Tester that requires no account at all — you can preview templates and download a real PNG client-side before committing to anything.&lt;/p&gt;

&lt;p&gt;The free tester is the fastest way to understand what you're buying. Paste your title, pick a template, see exactly what LinkedIn will display. No signup, no credit card, no friction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;Dynamic OG images are a competitive advantage. A link with a well-designed preview gets clicked; a bare URL gets scrolled past. The technical challenge is making generation secure, reliable, and affordable.&lt;/p&gt;

&lt;p&gt;URL-signed HMAC authentication solves the crawler problem. A dedicated render service with a watchdog solves the reliability problem. Year-long immutable caching solves the cost problem.&lt;/p&gt;

&lt;p&gt;The result is an API where the first request costs one credit and everything after is free — and that's a trade worth making.&lt;/p&gt;

</description>
      <category>api</category>
      <category>backend</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Implement Dynamic OG Images in Next.js, Remix, or Laravel: A Step-by-Step Guide</title>
      <dc:creator>Hugo Bernardo Cardoso</dc:creator>
      <pubDate>Mon, 17 Aug 2026 08:25:40 +0000</pubDate>
      <link>https://dev.to/hugo_bernardocardoso_d38/how-to-implement-dynamic-og-images-in-nextjs-remix-or-laravel-a-step-by-step-guide-4on7</link>
      <guid>https://dev.to/hugo_bernardocardoso_d38/how-to-implement-dynamic-og-images-in-nextjs-remix-or-laravel-a-step-by-step-guide-4on7</guid>
      <description>&lt;h1&gt;
  
  
  How to Implement Dynamic OG Images in Next.js, Remix, or Laravel: A Step-by-Step Guide
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Why Your Static OG Image Is Costing You Clicks
&lt;/h2&gt;

&lt;p&gt;When you share a link on LinkedIn, X, Slack, WhatsApp, or Facebook, the platform crawls your page and pulls the &lt;code&gt;og:image&lt;/code&gt; meta tag. If that image is static — the same generic banner for every URL — you're leaving clicks on the table.&lt;/p&gt;

&lt;p&gt;Here's the problem: a static OG image says nothing about &lt;em&gt;what&lt;/em&gt; the reader will get. "How to Implement Dynamic OG Images" with a generic gradient tells me nothing. "How to Implement Dynamic OG Images — 7 Steps, 15 Minutes, Zero Server Setup" tells me everything. The latter gets the click.&lt;/p&gt;

&lt;p&gt;Dynamic OG images — generated per-URL with the actual title, author, price, or rating baked in — convert better because they set accurate expectations. The crawler sees a preview that matches the content, so the person on the other end knows exactly what they're opening.&lt;/p&gt;

&lt;p&gt;But here's the catch: &lt;strong&gt;social crawlers don't execute JavaScript&lt;/strong&gt;. Client-side rendering fails silently. WhatsApp, Twitter, and LinkedIn all fetch your URL server-side and read the raw HTML. If your OG image is generated client-side, you get a blank preview or a broken image.&lt;/p&gt;

&lt;p&gt;The fix is a server-side endpoint that returns a real image. FastOG does exactly this: you call one URL with query parameters, and you get back a ready-to-share 1200×630 PNG, JPEG, or WebP — rendered server-side by headless Chrome, not assembled client-side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites: What You Need Before You Start
&lt;/h2&gt;

&lt;p&gt;Before you wire anything up, you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A FastOG account&lt;/strong&gt; — you get 100 free credits on signup, no credit card required&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your API key and secret&lt;/strong&gt; — found in your dashboard; each key has its own HMAC secret&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A framework&lt;/strong&gt; — Next.js (App or Pages Router), Remix, or Laravel; the pattern is identical across all three&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A template ID&lt;/strong&gt; — FastOG ships 41 server-side Svelte 5 templates (&lt;code&gt;blog&lt;/code&gt;, &lt;code&gt;product&lt;/code&gt;, &lt;code&gt;pricing&lt;/code&gt;, &lt;code&gt;saaslaunch&lt;/code&gt;, &lt;code&gt;stats&lt;/code&gt;, &lt;code&gt;bento&lt;/code&gt;, &lt;code&gt;comparison&lt;/code&gt;, &lt;code&gt;countdown&lt;/code&gt;, &lt;code&gt;promo&lt;/code&gt;, &lt;code&gt;ecommerce&lt;/code&gt;, &lt;code&gt;game&lt;/code&gt;, &lt;code&gt;news&lt;/code&gt;, &lt;code&gt;podcast&lt;/code&gt;, &lt;code&gt;video&lt;/code&gt;, &lt;code&gt;devblog&lt;/code&gt;, &lt;code&gt;codepost&lt;/code&gt;, and more)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The core endpoint is &lt;code&gt;GET /api/v1/og&lt;/code&gt;. Each render costs 1 credit, and the image is cached for a year (&lt;code&gt;Cache-Control: public, max-age=31536000, immutable&lt;/code&gt;), so everyone after the first request gets it for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Set Up Your Dynamic Image Endpoint
&lt;/h2&gt;

&lt;p&gt;The pattern is the same in every framework: accept the incoming request, build the OG image URL with your parameters, sign it, and return it as the &lt;code&gt;og:image&lt;/code&gt; meta tag.&lt;/p&gt;

&lt;p&gt;Here's the canonical flow in &lt;strong&gt;Laravel&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// routes/web.php&lt;/span&gt;
&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/post/{slug}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$slug&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'slug'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$slug&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;firstOrFail&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nv"&gt;$ogUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'https://api.fastog.com/api/v1/og?'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;http_build_query&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'template'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'blog'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'title'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'subtitle'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;excerpt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'author'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;author_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'key'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'services.fastog.key'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'s'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;sign_og_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// see Step 4&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'post'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'post'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'ogImage'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$ogUrl&lt;/span&gt;&lt;span class="p"&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;In &lt;strong&gt;Next.js App Router&lt;/strong&gt;, you'd do the same inside a &lt;code&gt;generateMetadata&lt;/code&gt; function:&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="c1"&gt;// app/post/[slug]/page.tsx&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateMetadata&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;}):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Metadata&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ogUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildOgUrl&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;subtitle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;excerpt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author_name&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="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;openGraph&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ogUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;630&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="p"&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;And in &lt;strong&gt;Remix&lt;/strong&gt;, inside a &lt;code&gt;loader&lt;/code&gt; or &lt;code&gt;meta&lt;/code&gt; export:&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;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MetaFunction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;data&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;property&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;og:image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;buildOgUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&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;The key insight: &lt;strong&gt;the OG image URL is just a URL&lt;/strong&gt;. Any framework that can emit a &lt;code&gt;&amp;lt;meta&amp;gt;&lt;/code&gt; tag can use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Design Your Template and Pass Data Safely
&lt;/h2&gt;

&lt;p&gt;FastOG renders templates server-side with Svelte 5 at exactly 1200×630 using headless Chrome (Browsershot) in a separate Node render service. You don't touch the rendering pipeline — you just pass data.&lt;/p&gt;

&lt;p&gt;The available query parameters are: &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;subtitle&lt;/code&gt;, &lt;code&gt;template&lt;/code&gt;, &lt;code&gt;price&lt;/code&gt;, &lt;code&gt;original_price&lt;/code&gt;, &lt;code&gt;rating&lt;/code&gt;, &lt;code&gt;features&lt;/code&gt;, &lt;code&gt;badge&lt;/code&gt;, &lt;code&gt;image&lt;/code&gt;, and more, depending on the template.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security rule: treat the OG URL as untrusted input.&lt;/strong&gt; Never pass raw user content without encoding. FastOG signs the canonical sorted query with RFC 3986 encoding, so the signature covers every parameter. If you're building the URL yourself, use &lt;code&gt;URLSearchParams&lt;/code&gt; or &lt;code&gt;http_build_query&lt;/code&gt; — don't hand-concatenate strings.&lt;/p&gt;

&lt;p&gt;A safe pattern:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildOgUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&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="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;search&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FASTOG_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;s&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// HMAC signature&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`https://api.fastog.com/api/v1/og?&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&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;h2&gt;
  
  
  Step 3: Handle Caching and Edge Storage
&lt;/h2&gt;

&lt;p&gt;This is where FastOG's economics get interesting. Every render costs 1 credit, but the image is cached for a year. The first request for a given URL pays the credit; everyone after that gets the cached copy for free.&lt;/p&gt;

&lt;p&gt;The response includes &lt;code&gt;Cache-Control: public, max-age=31536000, immutable&lt;/code&gt;. That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Social crawlers&lt;/strong&gt; (LinkedIn, Twitter, Facebook) hit the cached copy — fast, no credit cost&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your users&lt;/strong&gt; sharing the link also hit the cache&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You&lt;/strong&gt; don't pay for repeat renders&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're generating OG images for a high-traffic site, this is the difference between $0.0014 per image and effectively $0 after the first render.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Add HMAC Signing for Security
&lt;/h2&gt;

&lt;p&gt;This is the part most tutorials skip, and it's the one that matters. FastOG requires every request to carry &lt;code&gt;key&lt;/code&gt; + &lt;code&gt;s&lt;/code&gt; (signature). The signature lives in the URL, not in headers, because social network crawlers don't send custom headers.&lt;/p&gt;

&lt;p&gt;The signing algorithm:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Take all query parameters except &lt;code&gt;s&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Sort them alphabetically by key&lt;/li&gt;
&lt;li&gt;RFC 3986-encode each key and value&lt;/li&gt;
&lt;li&gt;Concatenate as &lt;code&gt;key=value&amp;amp;key=value&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;HMAC-SHA256 the result with your API secret&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's a working implementation in &lt;strong&gt;Node.js&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&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;crypto&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;signOgUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&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="kr"&gt;string&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;secret&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="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sorted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;s&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="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;])}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&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;p&gt;And in &lt;strong&gt;PHP&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;signOgUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$secret&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;ksort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$canonical&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;http_build_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&amp;amp;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;PHP_QUERY_RFC3986&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;hash_hmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sha256'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$canonical&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$secret&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;Why bother? Because without signing, anyone can burn your credits by hitting your endpoint with arbitrary parameters. With signing, only requests you authorized render.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Test Across All Major Platforms
&lt;/h2&gt;

&lt;p&gt;Different platforms cache OG images differently. Here's what to check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LinkedIn&lt;/strong&gt;: notoriously aggressive caching. Use their Post Inspector to force a refresh.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;X (Twitter)&lt;/strong&gt;: use the Card Validator. It shows you exactly what the crawler sees.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WhatsApp&lt;/strong&gt;: the most frustrating — it caches aggressively and has no public debugger. Append &lt;code&gt;?v=2&lt;/code&gt; to your URL to bust the cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Facebook&lt;/strong&gt;: the Sharing Debugger lets you scrape fresh and see errors.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;FastOG also has a free OG Image Tester — no signup required — that previews your image client-side and downloads a real PNG. It's the fastest way to sanity-check a template before you deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Go Live with Monitoring and Analytics
&lt;/h2&gt;

&lt;p&gt;Once you're live, watch these signals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;X-Renders-Remaining&lt;/code&gt;&lt;/strong&gt; header on every response — tells you your credit balance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;X-FastOG-Watermark&lt;/code&gt;&lt;/strong&gt; header — confirms whether the image is watermarked&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error codes&lt;/strong&gt; — &lt;code&gt;402&lt;/code&gt; (insufficient credits), &lt;code&gt;422&lt;/code&gt; (validation error), &lt;code&gt;502&lt;/code&gt; (render service unreachable), &lt;code&gt;503&lt;/code&gt; (service unavailable). Errors cost nothing — deduction happens only after a successful render, with an atomic lock preventing double-spend.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you see a spike in &lt;code&gt;502&lt;/code&gt;s, the render service may have wedged. FastOG runs a self-healing watchdog (&lt;code&gt;og:health&lt;/code&gt;) that auto-restarts the Docker container, but you should still monitor your error rate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting Common Issues: Empty Previews, Caching, and Quotas
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"My preview is blank on WhatsApp but works on X"&lt;/strong&gt; — WhatsApp caches aggressively and doesn't honor cache-busting headers. Append a query param (&lt;code&gt;?v=timestamp&lt;/code&gt;) to force a fresh fetch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"The image is stale on LinkedIn"&lt;/strong&gt; — Use LinkedIn's Post Inspector to force a re-scrape. It's the only reliable way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"I'm out of credits"&lt;/strong&gt; — Check &lt;code&gt;X-Renders-Remaining&lt;/code&gt;. Cached images are free, so the fix is usually to ensure your cache headers are being respected. If you're genuinely out, credit packs start at $2.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"My signature doesn't validate"&lt;/strong&gt; — The most common cause is encoding mismatch. Make sure you're using RFC 3986 encoding (not &lt;code&gt;encodeURIComponent&lt;/code&gt;'s default) and that your parameters are sorted before signing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"I'm getting 422s"&lt;/strong&gt; — You're passing an invalid parameter or template name. Check the template list and your parameter names against the API reference.&lt;/p&gt;




&lt;p&gt;Dynamic OG images are a small change with an outsized impact on click-through. The pattern is identical whether you're on Next.js, Remix, or Laravel: build a signed URL, emit it as &lt;code&gt;og:image&lt;/code&gt;, and let the server-side renderer do the work. FastOG handles the rendering, caching, and security — you just ship the meta tag.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Android Strings XML Translation Online: The No-Login Tool That Preserves Your Variables</title>
      <dc:creator>Hugo Bernardo Cardoso</dc:creator>
      <pubDate>Sun, 16 Aug 2026 09:56:57 +0000</pubDate>
      <link>https://dev.to/hugo_bernardocardoso_d38/android-strings-xml-translation-online-the-no-login-tool-that-preserves-your-variables-2hcc</link>
      <guid>https://dev.to/hugo_bernardocardoso_d38/android-strings-xml-translation-online-the-no-login-tool-that-preserves-your-variables-2hcc</guid>
      <description>&lt;h1&gt;
  
  
  Android Strings XML Translation Online: The No-Login Tool That Preserves Your Variables
&lt;/h1&gt;

&lt;p&gt;You're solo. You're shipping in weeks. Your &lt;code&gt;strings.xml&lt;/code&gt; file has 1,200 lines, and 14 of them contain &lt;code&gt;%1$s&lt;/code&gt; or &lt;code&gt;{player_name}&lt;/code&gt; placeholders that will crash your app if a translator touches them.&lt;/p&gt;

&lt;p&gt;Agencies want $2,000 and three weeks. ChatGPT mangles your variable tags, skips long strings, and you're stuck fixing broken JSON at 2 AM.&lt;/p&gt;

&lt;p&gt;LocaFile AI is the android strings XML translation online tool built for exactly this moment. No login. No subscription. No per-word haggling. You upload your file, pay a flat fee, and get a ZIP back with every string translated and every variable locked in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  What LocaFile AI Actually Does
&lt;/h2&gt;

&lt;p&gt;LocaFile AI is a no-login, pay-per-pass AI game localization web app. It's built for solo developers and small indie studios shipping on Steam — the people who need 8 languages for a Steam Next Fest demo but don't have a localization budget.&lt;/p&gt;

&lt;p&gt;The core flow is dead simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Upload&lt;/strong&gt; — drag and drop your game string file. The parser detects the format, counts words, and validates structure. No account required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preview for free&lt;/strong&gt; — paste a few lines and run a free syntax check. You can even test a real AI translation pass on up to 5 lines before paying anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pay once&lt;/strong&gt; — a flat fee via Stripe. You get an entitlement token, not a user account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Get your ZIP&lt;/strong&gt; — the translation job runs through a three-pass AI pipeline, bundles everything, and delivers it via on-screen download plus email.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The entire process takes minutes, not weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Variable Problem: Why Most Translation Tools Break Your Game
&lt;/h2&gt;

&lt;p&gt;Here's the thing nobody tells you about android string translation: it's not the words that break your app. It's the variables.&lt;/p&gt;

&lt;p&gt;Your &lt;code&gt;strings.xml&lt;/code&gt; probably contains entries like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;string&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"welcome_message"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Welcome back, %1$s! You have {unread_count} new messages.&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;string&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"health_display"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;color&lt;/span&gt;&lt;span class="err"&gt;=red&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;HP:&lt;span class="nt"&gt;&amp;lt;/color&amp;gt;&lt;/span&gt; {current_hp}/{max_hp}&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A generic translation service sees &lt;code&gt;%1$s&lt;/code&gt;, &lt;code&gt;{unread_count}&lt;/code&gt;, and &lt;code&gt;&amp;lt;color=red&amp;gt;&lt;/code&gt; as text to translate. It "helpfully" rewrites them, reorders them, or drops them entirely. The result? Your app crashes on launch, or worse, displays &lt;code&gt;[[MISSING]]&lt;/code&gt; placeholders to players in 8 languages.&lt;/p&gt;

&lt;p&gt;This is the problem LocaFile AI was built to solve. The core differentiator is a &lt;strong&gt;VariableLocker&lt;/strong&gt; system with 10 variable-matcher presets: &lt;code&gt;double_curly&lt;/code&gt;, &lt;code&gt;dollar_curly&lt;/code&gt;, &lt;code&gt;single_curly&lt;/code&gt;, &lt;code&gt;square_bracket&lt;/code&gt;, &lt;code&gt;smart_string&lt;/code&gt;, &lt;code&gt;printf&lt;/code&gt;, &lt;code&gt;rich_text&lt;/code&gt;, &lt;code&gt;laravel_colon&lt;/code&gt;, &lt;code&gt;button_glyph&lt;/code&gt;, and &lt;code&gt;subtitle_tag&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Before the AI ever sees your text, every variable is swapped for a &lt;code&gt;[[LOCK_N]]&lt;/code&gt; token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before: "Welcome back, %1$s! You have {unread_count} new messages."
After:   "Welcome back, [[LOCK_1]]! You have [[LOCK_2]] new messages."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI translates the surrounding text. Then the locker restores your variables byte-for-byte. A validator checks every single one. You get a per-language QA report proving nothing was mangled.&lt;/p&gt;

&lt;p&gt;This matters for every format, not just Android. The same engine handles Godot CSV/TSV/PO files, Unity CSV/YAML/JSON, Ren'Py scripts, Unreal projects, Flutter ARB files, and subtitle formats like SRT, VTT, ASS, and SBV. The android strings.xml translation online workflow is just one of 17 format parsers under the hood.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Android Strings XML Translation Workflow
&lt;/h2&gt;

&lt;p&gt;Let's walk through the actual android translate strings.xml process.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Upload and Analyze
&lt;/h3&gt;

&lt;p&gt;Go to locafileai.com, drag your &lt;code&gt;strings.xml&lt;/code&gt; onto the upload zone. The parser detects the format, validates the XML structure, counts words, and gives you a price. No signup form. No "create an account to see pricing" wall.&lt;/p&gt;

&lt;p&gt;If you're not sure whether your file is valid, paste a snippet into the free syntax checker at &lt;code&gt;/analyze&lt;/code&gt;. It'll flag malformed XML, duplicate keys, or unescaped quotes before you spend a cent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Free Preview
&lt;/h3&gt;

&lt;p&gt;This is where LocaFile AI differs from every android string translation tool that makes you pay upfront and pray. You can lock variables on your pasted string with zero AI cost, then run one real AI pass on up to 5 lines. You'll see exactly how the variable locking works before you commit.&lt;/p&gt;

&lt;p&gt;The preview is throttled at 10 requests per minute because every AI call costs money — but for a 5-line test, you'll never hit that limit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Pay and Translate
&lt;/h3&gt;

&lt;p&gt;Pick your tier. The &lt;strong&gt;Indie Starter&lt;/strong&gt; at $19 is an impulse buy for solo devs prepping a Steam Next Fest demo. The &lt;strong&gt;Steam Launchpad&lt;/strong&gt; at $99 fits small studios shipping a full launch. The &lt;strong&gt;Studio Pass&lt;/strong&gt; at $249 covers multi-SKU and DLC workflows.&lt;/p&gt;

&lt;p&gt;Pay through Stripe Checkout. The webhook creates your entitlement — a UUID, not a user account — and dispatches the translation job.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: The Three-Pass Pipeline
&lt;/h3&gt;

&lt;p&gt;Your file doesn't get one pass through an AI model. It gets three:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pass one&lt;/strong&gt; locks all variables and does the initial translation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pass two&lt;/strong&gt; reviews for consistency, context, and tone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pass three&lt;/strong&gt; validates every variable was restored correctly and runs quality checks.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each language gets a &lt;code&gt;TranslationQaReport&lt;/code&gt;. If a variable was dropped, the pipeline catches it before you ever download the file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Download and Ship
&lt;/h3&gt;

&lt;p&gt;You get a ZIP with all your translated &lt;code&gt;strings.xml&lt;/code&gt; files, organized by locale. The download link expires after 24 hours, but you'll also get the files via email. Lose the link? The &lt;code&gt;/recover&lt;/code&gt; endpoint regenerates it from your UUID.&lt;/p&gt;

&lt;p&gt;If a job fails partway through, you can retry without re-paying — the pipeline resumes from the completed languages instead of starting over.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Beats the Alternatives
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Agencies&lt;/strong&gt; deliver quality but cost $2,000+ and take weeks. You don't have weeks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Raw ChatGPT&lt;/strong&gt; is free but will mangle your &lt;code&gt;{player_name}&lt;/code&gt; tags, skip long strings, and produce inconsistent terminology across 8 languages. You'll spend more time fixing broken XML than you saved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Other online tools&lt;/strong&gt; treat translation as a text swap. They don't understand game string formats, variable placeholders, or the difference between &lt;code&gt;%1$s&lt;/code&gt; and &lt;code&gt;%s&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;LocaFile AI is the android strings xml translation tool that treats your variables as inviolable. The entire architecture — the VariableLocker, the three-pass pipeline, the QA reports — exists to guarantee one thing: &lt;strong&gt;your app won't crash when translated.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;If you're a solo dev or small studio shipping on Steam, you need android strings XML translation online that doesn't require an account, doesn't charge per word, and won't break your game. &lt;a href="https://locafileai.com" rel="noopener noreferrer"&gt;LocaFile AI&lt;/a&gt; delivers exactly that: upload, pay a flat fee, download your ZIP, ship your game.&lt;/p&gt;

&lt;p&gt;No login. No subscription. No fixing broken tags at 2 AM.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to Generate OG Images at Scale: A Developer's Guide</title>
      <dc:creator>Hugo Bernardo Cardoso</dc:creator>
      <pubDate>Sat, 15 Aug 2026 14:55:45 +0000</pubDate>
      <link>https://dev.to/hugo_bernardocardoso_d38/how-to-generate-og-images-at-scale-a-developers-guide-56pl</link>
      <guid>https://dev.to/hugo_bernardocardoso_d38/how-to-generate-og-images-at-scale-a-developers-guide-56pl</guid>
      <description>&lt;h1&gt;
  
  
  How to Generate OG Images at Scale: A Developer's Guide
&lt;/h1&gt;

&lt;p&gt;The link preview is the first thing people see before they click. On X, LinkedIn, Slack, and iMessage, your URL renders as a 1200×630 card — and if that card is a generic gray box with your domain name, you've already lost the click to the person who posted a screenshot instead.&lt;/p&gt;

&lt;p&gt;Dynamic Open Graph images fix this. Instead of one static PNG per page, you render a unique card per URL — title, author, reading time, a chart, whatever your page actually contains. The result is a measurable lift in click-through rate and share velocity. But &lt;em&gt;how&lt;/em&gt; you generate those images matters more than whether you do it at all. Get the architecture wrong and you'll ship broken cards, burn server resources, or lock yourself into a tool that can't scale.&lt;/p&gt;

&lt;p&gt;Here's the landscape, the trade-offs, and a pipeline that holds up under real traffic.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Landscape: Client-Side vs. Server-Side Rendering
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Client-side generation&lt;/strong&gt; is the trap. Tools like myogimage.com let you design a template in the browser and export a static PNG. That's fine for a one-off — but it collapses the moment you need dynamic content. You can't render a card for a blog post that doesn't exist yet, and you can't automate a pipeline around a GUI. Worse, some of these "free" tools advertise an API that doesn't actually exist. If your OG strategy depends on a service that can't be called programmatically, you don't have a strategy — you have a screenshot.&lt;/p&gt;

&lt;p&gt;The other client-side trap is generating images in the browser at request time. Social scrapers (Twitterbot, Slackbot, LinkedInBot) don't execute JavaScript. If your OG image is rendered client-side, the scraper sees nothing. This is the single most common reason "my OG image works in the preview but not when I share it."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server-side rendering&lt;/strong&gt; is the only approach that works reliably. The scraper makes a GET request, your server returns a complete PNG. No JavaScript required. Within server-side, you have two real options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;DIY with &lt;code&gt;satori&lt;/code&gt; + a deployment platform&lt;/strong&gt; (the Vercel approach). &lt;code&gt;satori&lt;/code&gt; converts JSX to SVG, then you rasterize to PNG with &lt;code&gt;resvg&lt;/code&gt; or &lt;code&gt;sharp&lt;/code&gt;. It's fast, it's free, and it's fully under your control. The cost is yours too: you maintain the rendering service, handle font loading, manage concurrency, and debug edge cases yourself. It's a solid choice if you have the time and the traffic justifies the infrastructure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A dedicated OG image API&lt;/strong&gt; (like FastOG). You send a GET request with your template and parameters, the service renders the image server-side, caches it at the edge, and returns a URL. No infrastructure to maintain, no fonts to bundle, no scraper compatibility to test. You pay per image, and caching means repeat renders cost nothing.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The right choice depends on your constraints. If you're an indie hacker shipping a blog this weekend, the DIY stack is a fun afternoon project. If you're building a SaaS where every page needs a unique card and your time is better spent on the product, a managed API wins.&lt;/p&gt;




&lt;h2&gt;
  
  
  Anatomy of a High-Performance OG Image Pipeline
&lt;/h2&gt;

&lt;p&gt;A production-grade pipeline has five stages. Here's what each one needs to do.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. URL Design
&lt;/h3&gt;

&lt;p&gt;Your OG image endpoint should be a simple GET request with query parameters — no auth headers, no POST bodies. Social scrapers only send GET requests. FastOG's pattern looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://api.fastog.com/api/v1/og?template=blog&amp;amp;title=Hello%20World&amp;amp;author=Jane
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the parameter surface small. Every parameter is a template variable you need to document, validate, and test. Start with 3–5 per template.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Template Registry
&lt;/h3&gt;

&lt;p&gt;A template is a design with named slots: &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;author&lt;/code&gt;, &lt;code&gt;date&lt;/code&gt;, &lt;code&gt;reading_time&lt;/code&gt;, &lt;code&gt;accent_color&lt;/code&gt;. Your registry maps a template ID to its layout and default styles. FastOG ships 41 templates covering OG cards (1200×630) and X headers (1500×500) — but you don't need 41. You need a handful that match your brand and a registry that makes adding new ones trivial.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Rendering
&lt;/h3&gt;

&lt;p&gt;The renderer takes the template, injects the parameters, and produces a PNG. This is where &lt;code&gt;satori&lt;/code&gt;-style approaches and managed APIs diverge. The key performance metric is &lt;strong&gt;time-to-first-byte&lt;/strong&gt;: social scrapers are impatient. If your render takes longer than ~2 seconds, platforms will fall back to a generic card. FastOG renders via Svelte → headless Chrome, which handles complex layouts and custom fonts gracefully, and serves the result from edge cache.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Caching
&lt;/h3&gt;

&lt;p&gt;This is the difference between "paying per image" and "paying per unique image." If your pipeline caches at the CDN edge, the first render of a URL is the only render that costs anything. Every subsequent scrape — and there will be many, every time someone shares the link — hits the cache. FastOG's model is 1 credit = 1 image, with 0 cost on cache hits. If you're DIY, make sure your cache layer is in front of your renderer, not behind it.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Security
&lt;/h3&gt;

&lt;p&gt;If your OG endpoint accepts query parameters, it's a public URL. Anyone can hit it. That's fine for public content, but it means you need &lt;strong&gt;HMAC signing&lt;/strong&gt; for anything you don't want rendered arbitrarily. The pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Node.js example: sign an OG image URL&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;signOgUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;sig=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Usage&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signOgUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.fastog.com/api/v1/og&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;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&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 World&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OG_SECRET&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server recomputes the HMAC and rejects requests with an invalid or missing signature. This prevents abuse of your render quota and stops people from generating arbitrary images on your dime.&lt;/p&gt;




&lt;h2&gt;
  
  
  Putting It Together
&lt;/h2&gt;

&lt;p&gt;Here's a minimal implementation for a blog, using a managed API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/og.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OG_BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.fastog.com/api/v1/og&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OG_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OG_SECRET&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;getOgImageUrl&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;excerpt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;excerpt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;OG_SECRET&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;OG_BASE&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;sig=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;Then in your page template, add the meta tags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:title"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;{title}&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:image"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;{getOgImageUrl({&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;excerpt&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt; &lt;span class="err"&gt;})}&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. The scraper fetches the image URL, your API renders and caches it, and every share of that page shows a custom card.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;Client-side OG tools are fine for a static site with five pages. The moment you need dynamic cards — per-post, per-user, per-product — you need a server-side pipeline. Whether you build it with &lt;code&gt;satori&lt;/code&gt; or buy it from a managed API depends on your time budget and traffic. But the architecture is non-negotiable: GET-based URL, template registry, fast renderer, edge cache, and HMAC signing.&lt;/p&gt;

&lt;p&gt;Start with one template and one page type. Measure your click-through rate before and after. When you see the lift, expand to the rest of your site. The first card is the hardest — everything after that is just filling in the template.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to Generate OG Images at Scale: A Developer's Guide</title>
      <dc:creator>Hugo Bernardo Cardoso</dc:creator>
      <pubDate>Sat, 15 Aug 2026 14:52:36 +0000</pubDate>
      <link>https://dev.to/hugo_bernardocardoso_d38/how-to-generate-og-images-at-scale-a-developers-guide-k4h</link>
      <guid>https://dev.to/hugo_bernardocardoso_d38/how-to-generate-og-images-at-scale-a-developers-guide-k4h</guid>
      <description>&lt;h1&gt;
  
  
  How to Generate OG Images at Scale: A Developer's Guide
&lt;/h1&gt;

&lt;p&gt;The link preview is the first thing people see before they click. On X, LinkedIn, Slack, and iMessage, your URL renders as a 1200×630 card — and if that card is a generic gray box with your domain name, you've already lost the click to the person who posted a screenshot instead.&lt;/p&gt;

&lt;p&gt;Dynamic Open Graph images fix this. Instead of one static PNG per page, you render a unique card per URL — title, author, reading time, a chart, whatever your page actually contains. The result is a measurable lift in click-through rate and share velocity. But &lt;em&gt;how&lt;/em&gt; you generate those images matters more than whether you do it at all. Get the architecture wrong and you'll ship broken cards, burn server resources, or lock yourself into a tool that can't scale.&lt;/p&gt;

&lt;p&gt;Here's the landscape, the trade-offs, and a pipeline that holds up under real traffic.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Landscape: Client-Side vs. Server-Side Rendering
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Client-side generation&lt;/strong&gt; is the trap. Tools like myogimage.com let you design a template in the browser and export a static PNG. That's fine for a one-off — but it collapses the moment you need dynamic content. You can't render a card for a blog post that doesn't exist yet, and you can't automate a pipeline around a GUI. Worse, some of these "free" tools advertise an API that doesn't actually exist. If your OG strategy depends on a service that can't be called programmatically, you don't have a strategy — you have a screenshot.&lt;/p&gt;

&lt;p&gt;The other client-side trap is generating images in the browser at request time. Social scrapers (Twitterbot, Slackbot, LinkedInBot) don't execute JavaScript. If your OG image is rendered client-side, the scraper sees nothing. This is the single most common reason "my OG image works in the preview but not when I share it."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server-side rendering&lt;/strong&gt; is the only approach that works reliably. The scraper makes a GET request, your server returns a complete PNG. No JavaScript required. Within server-side, you have two real options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;DIY with &lt;code&gt;satori&lt;/code&gt; + a deployment platform&lt;/strong&gt; (the Vercel approach). &lt;code&gt;satori&lt;/code&gt; converts JSX to SVG, then you rasterize to PNG with &lt;code&gt;resvg&lt;/code&gt; or &lt;code&gt;sharp&lt;/code&gt;. It's fast, it's free, and it's fully under your control. The cost is yours too: you maintain the rendering service, handle font loading, manage concurrency, and debug edge cases yourself. It's a solid choice if you have the time and the traffic justifies the infrastructure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A dedicated OG image API&lt;/strong&gt; (like FastOG). You send a GET request with your template and parameters, the service renders the image server-side, caches it at the edge, and returns a URL. No infrastructure to maintain, no fonts to bundle, no scraper compatibility to test. You pay per image, and caching means repeat renders cost nothing.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The right choice depends on your constraints. If you're an indie hacker shipping a blog this weekend, the DIY stack is a fun afternoon project. If you're building a SaaS where every page needs a unique card and your time is better spent on the product, a managed API wins.&lt;/p&gt;




&lt;h2&gt;
  
  
  Anatomy of a High-Performance OG Image Pipeline
&lt;/h2&gt;

&lt;p&gt;A production-grade pipeline has five stages. Here's what each one needs to do.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. URL Design
&lt;/h3&gt;

&lt;p&gt;Your OG image endpoint should be a simple GET request with query parameters — no auth headers, no POST bodies. Social scrapers only send GET requests. FastOG's pattern looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://api.fastog.com/api/v1/og?template=blog&amp;amp;title=Hello%20World&amp;amp;author=Jane
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the parameter surface small. Every parameter is a template variable you need to document, validate, and test. Start with 3–5 per template.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Template Registry
&lt;/h3&gt;

&lt;p&gt;A template is a design with named slots: &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;author&lt;/code&gt;, &lt;code&gt;date&lt;/code&gt;, &lt;code&gt;reading_time&lt;/code&gt;, &lt;code&gt;accent_color&lt;/code&gt;. Your registry maps a template ID to its layout and default styles. FastOG ships 41 templates covering OG cards (1200×630) and X headers (1500×500) — but you don't need 41. You need a handful that match your brand and a registry that makes adding new ones trivial.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Rendering
&lt;/h3&gt;

&lt;p&gt;The renderer takes the template, injects the parameters, and produces a PNG. This is where &lt;code&gt;satori&lt;/code&gt;-style approaches and managed APIs diverge. The key performance metric is &lt;strong&gt;time-to-first-byte&lt;/strong&gt;: social scrapers are impatient. If your render takes longer than ~2 seconds, platforms will fall back to a generic card. FastOG renders via Svelte → headless Chrome, which handles complex layouts and custom fonts gracefully, and serves the result from edge cache.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Caching
&lt;/h3&gt;

&lt;p&gt;This is the difference between "paying per image" and "paying per unique image." If your pipeline caches at the CDN edge, the first render of a URL is the only render that costs anything. Every subsequent scrape — and there will be many, every time someone shares the link — hits the cache. FastOG's model is 1 credit = 1 image, with 0 cost on cache hits. If you're DIY, make sure your cache layer is in front of your renderer, not behind it.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Security
&lt;/h3&gt;

&lt;p&gt;If your OG endpoint accepts query parameters, it's a public URL. Anyone can hit it. That's fine for public content, but it means you need &lt;strong&gt;HMAC signing&lt;/strong&gt; for anything you don't want rendered arbitrarily. The pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Node.js example: sign an OG image URL&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;signOgUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;sig=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Usage&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signOgUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.fastog.com/api/v1/og&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;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&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 World&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OG_SECRET&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server recomputes the HMAC and rejects requests with an invalid or missing signature. This prevents abuse of your render quota and stops people from generating arbitrary images on your dime.&lt;/p&gt;




&lt;h2&gt;
  
  
  Putting It Together
&lt;/h2&gt;

&lt;p&gt;Here's a minimal implementation for a blog, using a managed API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/og.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OG_BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.fastog.com/api/v1/og&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OG_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OG_SECRET&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;getOgImageUrl&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;excerpt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;excerpt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;OG_SECRET&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;OG_BASE&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;sig=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;Then in your page template, add the meta tags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:title"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;{title}&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:image"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;{getOgImageUrl({&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;excerpt&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt; &lt;span class="err"&gt;})}&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. The scraper fetches the image URL, your API renders and caches it, and every share of that page shows a custom card.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;Client-side OG tools are fine for a static site with five pages. The moment you need dynamic cards — per-post, per-user, per-product — you need a server-side pipeline. Whether you build it with &lt;code&gt;satori&lt;/code&gt; or buy it from a managed API depends on your time budget and traffic. But the architecture is non-negotiable: GET-based URL, template registry, fast renderer, edge cache, and HMAC signing.&lt;/p&gt;

&lt;p&gt;Start with one template and one page type. Measure your click-through rate before and after. When you see the lift, expand to the rest of your site. The first card is the hardest — everything after that is just filling in the template.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Translate Game Localization Files (.JSON / .PO) Without Breaking Code Variables</title>
      <dc:creator>Hugo Bernardo Cardoso</dc:creator>
      <pubDate>Mon, 27 Jul 2026 18:38:44 +0000</pubDate>
      <link>https://dev.to/hugo_bernardocardoso_d38/how-to-translate-game-localization-files-json-po-without-breaking-code-variables-2334</link>
      <guid>https://dev.to/hugo_bernardocardoso_d38/how-to-translate-game-localization-files-json-po-without-breaking-code-variables-2334</guid>
      <description>&lt;p&gt;Introduction: The Scale vs. Code Integrity Dilemma&lt;br&gt;
Localizing an indie game or a software product is no longer optional. On platforms like Steam, offering the standard 28-language catalogue (including Simplified Chinese, Japanese, Korean, German, and Brazilian Portuguese) routinely unlocks over 50% of a game's total revenue potential.&lt;br&gt;
Yet, indie game developers face a brutal trade-off:&lt;br&gt;
Human Translation Agencies: Charging $\$0.10$ to $\$0.20$ per word. Localizing a modest 20,000-word game into 28 languages can easily cost $\$30,000+$, completely pricing out solo developers and small teams.&lt;br&gt;
Traditional Enterprise SaaS (Lokalise, Crowdin): Charging $\$50$ to $\$200+$ per month just for workspace access, forcing developers into bloated workflows and complex account setups.&lt;br&gt;
Naïve Single-Pass AI (Raw ChatGPT/Claude): Fast and cheap, but destructive to game builds.&lt;/p&gt;

&lt;p&gt;This article explores the technical breakdown of why standard LLMs fail when processing code files, and details the engineering architecture required to build a code-safe, zero-friction localization pipeline.&lt;br&gt;
Section 1: Anatomy of a Build Crash - Why Raw LLMs Corrupt&amp;nbsp;Code&lt;br&gt;
Large Language Models (LLMs) are statistical text predictors designed for natural human prose. They lack an internal compiler or Abstract Syntax Tree (AST) context when reading a raw file.&lt;br&gt;
When you paste a&amp;nbsp;.json,&amp;nbsp;.po,&amp;nbsp;.yaml, or&amp;nbsp;.csv file into a standard LLM, five distinct failure modes occur:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Variable &amp;amp; Token&amp;nbsp;Mutation
Game engines rely on runtime string interpolation. When an LLM encounters:
JSON
"hud_gold": "You collected {gold_amount} coins from {monster_type}."
It frequently translates the variable names inside the brackets:
JSON
"hud_gold": "Você coletou {quantidade_ouro} moedas de {tipo_monstro}." // ❌ CRASH: Engine looks for {gold_amount}&lt;/li&gt;
&lt;li&gt;Positional &amp;amp; Printf Specifier Mangling
C-style formatting specifiers like %s, %d, %f, or&amp;nbsp;:user are often dropped, reordered without positional indexes, or spaced out:
JSON
"save_slot": "Save file %s loaded." -&amp;gt; "Save file % s loaded." // ❌ PARSE ERROR&lt;/li&gt;
&lt;li&gt;ICU Plural Rule Destruction
Complex game strings use ICU plural syntax to handle numerical agreement:
Plaintext
{count, plural, =0 {No items} =1 {One item} other {# items}}
Standard LLMs frequently translate the structural control keywords (plural, other), breaking the engine's localization parser entirely.&lt;/li&gt;
&lt;li&gt;Rich Text &amp;amp; HTML Tag Pollution
Game engines (like Unity's TextMeshPro or Godot's RichTextLabel) use inline markup like  or [b]. LLMs often translate color names (), drop closing tags, or convert angle brackets into unescaped HTML entities (&amp;lt;color&amp;gt;).&lt;/li&gt;
&lt;li&gt;Unescaped Quotes and Structural Syntax&amp;nbsp;Errors
In JSON or YAML files, if a translated French or Spanish string contains an unescaped double quote (e.g., "desc": "L'épée de l' "héros" "), the resulting output becomes invalid JSON, breaking the entire game build at boot.
Section 2: Architecture of an Engine-Aware Localization Engine
To guarantee zero build breaks, the translation process must be decoupled from the raw file structure. The LLM must never be allowed to touch raw code variables.
Here is the technical architecture behind LocaFile AI:
Plaintext
┌────────────────┐     ┌───────────────────────┐     ┌───────────────────────┐
│ Source File    │ ──&amp;gt; │ Engine Regex Parser   │ ──&amp;gt; │ Token Masking         │
│ (JSON/PO/CSV)  │     │ Auto-Detect Format    │     │ {var} ──&amp;gt; [[LOCK_0]]  │
└────────────────┘     └───────────────────────┘     └───────────────────────┘
                                                             │
┌────────────────┐     ┌───────────────────────┐                 ▼
│ Delivered ZIP  │ &amp;lt;── │ Post-Processing QA    │ &amp;lt;── ┌───────────────────────┐
│ (28 Languages) │     │ AST Validation &amp;amp; Fix  │     │ 3-Pass AI Engine      │
└────────────────┘     └───────────────────────┘     │ + Lore &amp;amp; Tone Rules   │
                                                 └───────────────────────┘
A. Engine Format Auto-Detection &amp;amp; AST&amp;nbsp;Parsing
The system supports 9 engine formats out of the box:
Godot:&amp;nbsp;.po, Godot&amp;nbsp;.csv (multi-column
Unity: Unity&amp;nbsp;.csv, i18n&amp;nbsp;.json
Unreal &amp;amp; Mobile: XLIFF, iOS&amp;nbsp;.strings, Android&amp;nbsp;.xml
Web Frameworks:&amp;nbsp;.yaml,&amp;nbsp;.tsv, Laravel PHP lang arrays&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;An AST parser extracts translatable values while maintaining the exact key mapping, structural nested hierarchy, and array index positions.&lt;br&gt;
B. The Immutable Variable&amp;nbsp;Locker&lt;br&gt;
Before any prompt is constructed, a multi-stage Regex scanner identifies all control variables, HTML/rich-text tags, positional specifiers, and ICU blocks:&lt;br&gt;
Detection: Identifies patterns matching {...}, %s, %d,&amp;nbsp;:key, , [tag], and indexed placeholders {0}.&lt;br&gt;
Replacement: Replaces each detected variable with an immutable, deterministic lock token: [[LOCK_0]], [[LOCK_1]], etc.&lt;br&gt;
Why Double Square Brackets? Double square bracket tokens ([[LOCK_N]]) do not naturally occur in legitimate game dialogue or software strings. They prevent LLM tokenization distortion and survive the AI translation pass untouched.&lt;/p&gt;

&lt;p&gt;C. Lore Glossaries &amp;amp; Prompt Injection Protection&lt;br&gt;
Game lore uses proper nouns (character names, fictional planets, unique spells) that must remain identical across all languages.&lt;br&gt;
Lore Glossary Engine: Developers can pass up to 2,000 characters of proper nouns (e.g., "CyberCore, Eldoria, X-400").&lt;br&gt;
Security: Glossaries are validated against strict prompt-injection detection patterns before being appended as immutable system constraints.&lt;/p&gt;

&lt;p&gt;D. Context &amp;amp; Tone Customization&lt;br&gt;
Language register heavily impacts player immersion. A medieval RPG demands formal or archaic phrasing, while a modern shooter requires punchy, casual dialogue.&lt;br&gt;
Developers can specify target registers:&lt;br&gt;
Standard: Clean, neutral UI phrasing.&lt;br&gt;
Casual: Natural, conversational registers (e.g., informal "tu" in French/Spanish).&lt;br&gt;
Fantasy / Heroic: Immersive register for RPGs and story-driven titles.&lt;br&gt;
Sci-Fi / Cyberpunk: Stylized technical phrasing.&lt;/p&gt;

&lt;p&gt;Section 3: The Automated Post-Processing QA &amp;amp; Auto-Fix&amp;nbsp;Pipeline&lt;br&gt;
Even with prompt constraints, an production-grade system cannot rely solely on the LLM's output. A dedicated post-processing engine must validate every single key before packaging.&lt;/p&gt;


&lt;ol&gt;

&lt;li&gt;&lt;p&gt;Linguistic Variable Checker (Regex AST Validator)&lt;br&gt;&lt;br&gt;
Before delivering the final ZIP bundle, a dedicated post-processing engine compares the source string against the target translation across 5 variable patterns:&lt;br&gt;&lt;br&gt;
Curly Brackets: {player_name}&lt;br&gt;&lt;br&gt;
Colon Specifiers:&amp;nbsp;:attribute&lt;br&gt;&lt;br&gt;
Printf Tokens: %s, %d&lt;br&gt;&lt;br&gt;
Markup Tags: , &lt;b&gt;&lt;br&gt;&lt;br&gt;
Indexed Tokens: {0}, {1}&lt;/b&gt;&lt;/p&gt;&lt;/li&gt;
&lt;b&gt;&lt;br&gt;
&lt;li&gt;&lt;p&gt;Defect Detection &amp;amp; Auto-Fix&amp;nbsp;Engine&lt;br&gt;&lt;br&gt;
The QA validator flags three types of issues:&lt;br&gt;&lt;br&gt;
Missing Variables: The source string had {count}, but the translation dropped it.&lt;br&gt;&lt;br&gt;
Corrupted Variables: The AI altered the internal token (e.g., [[LOCK_0]] became [[LOCK_0_ES]]).&lt;br&gt;&lt;br&gt;
Extra Variables: Unsanitized tokens present in translation but absent in source.&lt;/p&gt;&lt;/li&gt;
&lt;br&gt;
&lt;/b&gt;
&lt;/ol&gt;
&lt;b&gt;

&lt;/b&gt;&lt;p&gt;Automated Re-Injection: When a missing variable is detected, the engine calculates its semantic position relative to surrounding words and automatically re-injects the missing {variable} into the translated string before generating a detailed report (quality-report.json).&lt;br&gt;
Section 4: Pay-Per-Pass vs. Monthly Subscriptions&lt;br&gt;
The SaaS industry is saturated with recurring monthly subscriptions that penalize developers during idle development phases. Indie game development is inherently bursty: you might localize heavily during a major launch or playtest, and then go months without needing translation updates.&lt;br&gt;
Why Pay-Per-Pass Wins for Indie Game&amp;nbsp;Devs:&lt;br&gt;
PRICING COMPARISON:&lt;br&gt;
Human Agencies&amp;nbsp;: $0.10 - $0.20/word | High Cost | Manual Variable Handling&lt;br&gt;
Enterprise SaaS&amp;nbsp;: $50 - $200+/month | Subscription| Complex Workspace Setup&lt;br&gt;
LocaFile AI&amp;nbsp;: From $19 (Flat) | Pay-per-Pass| Automated Variable Protection&lt;br&gt;
Indie Starter ($19): 3 runs, up to 5,000 words/file across 6 core languages (EFIGS + CJK).&lt;br&gt;
Steam Launchpad ($99): 10 runs, up to 25,000 words/file across all 28 Steam catalogue languages.&lt;br&gt;
Studio Pass ($249): 35 runs, up to 100,000 words/file for large-scale RPGs and studio pipelines.&lt;/p&gt;

&lt;p&gt;Section 5: Hands-On Tutorial - Localizing a Game in 3&amp;nbsp;Steps&lt;br&gt;
Step 1: Prepare Your Game&amp;nbsp;File&lt;br&gt;
Ensure your string keys are separated from translatable values in a supported format:&lt;br&gt;
JSON&lt;br&gt;
{&lt;br&gt;
  "quest_header": "Quest: Defeat {boss_name}",&lt;br&gt;
  "item_reward": "You earned {amount} gold coins!",&lt;br&gt;
  "dialogue_intro": "Welcome to Eldoria, traveler."&lt;br&gt;
}&lt;br&gt;
Step 2: Test Variable Protection (Zero-Cost Live&amp;nbsp;Preview)&lt;br&gt;
Navigate to locafileai.com and use the zero-cost Variable Protection Preview tool on the homepage.&lt;br&gt;
Paste your raw JSON snippet to observe the production VariableLocker instantly convert {boss_name} and  into protected [[LOCK_N]] tokens in real-time before initiating any pass.&lt;br&gt;
JSON&lt;br&gt;
// Real-time Variable Masking Output:&lt;br&gt;
{&lt;br&gt;
  "quest_header": "Quest: Defeat [[LOCK_0]]",&lt;br&gt;
  "item_reward": "You earned [[LOCK_1]][[LOCK_2]][[LOCK_3]] gold coins!",&lt;br&gt;
  "dialogue_intro": "Welcome to Eldoria, traveler."&lt;br&gt;
}&lt;br&gt;
Step 3: Run the Pass &amp;amp; Import to&amp;nbsp;Engine&lt;br&gt;
Drop File or ZIP Batch: Drag your&amp;nbsp;.json,&amp;nbsp;.po, or&amp;nbsp;.csv files directly onto the dropzone.&lt;br&gt;
Set Glossaries &amp;amp; Tone: Add proper nouns to your Lore Glossary and select your game's tone (e.g., Fantasy or Casual).&lt;br&gt;
Execute Pass: Select your target languages (or full 28 Steam catalogue) and watch live real-time WebSocket progress via Laravel Reverb.&lt;br&gt;
Export &amp;amp; Build: Download your validated ZIP bundle and drop it directly into your Godot res:// or Unity Assets/Resources/ directories. Zero syntax errors, zero build breaks.&lt;/p&gt;

&lt;p&gt;Conclusion&lt;br&gt;
Game localization should not require thousands of dollars in agency fees or hours spent debugging broken JSON brackets and mangled variable names. By isolating variables through pre-parsing AST Regex lockers, applying post-processing QA validation, and offering a fair pay-per-pass pricing model, developers can expand their games to a global Steam audience with total code safety.&lt;br&gt;
Test the real-time variable protection preview today at locafileai.com!&lt;/p&gt;



</description>
      <category>gamedev</category>
      <category>softwaredevelopment</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Localize a Godot Game Without Breaking Variable Tags</title>
      <dc:creator>Hugo Bernardo Cardoso</dc:creator>
      <pubDate>Mon, 27 Jul 2026 18:36:12 +0000</pubDate>
      <link>https://dev.to/hugo_bernardocardoso_d38/how-to-localize-a-godot-game-without-breaking-variable-tags-3gm8</link>
      <guid>https://dev.to/hugo_bernardocardoso_d38/how-to-localize-a-godot-game-without-breaking-variable-tags-3gm8</guid>
      <description>&lt;h1&gt;
  
  
  How to Localize a Godot Game Without Breaking Variable Tags
&lt;/h1&gt;

&lt;p&gt;Godot's localization system is built around a simple idea: separate your text from your code, translate the text, and let the engine swap it at runtime. In practice, the variable tags inside those strings are where things fall apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Godot handles translatable strings
&lt;/h2&gt;

&lt;p&gt;Godot, a &lt;a href="https://godotengine.org/features/" rel="noopener noreferrer"&gt;free, open-source game engine&lt;/a&gt; used by solo developers and small studios for 2D and 3D projects, stores translatable text in CSV files or .po (gettext) files. You define a key, write your source string, and add columns or entries for each target language.&lt;/p&gt;

&lt;p&gt;A typical CSV row might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="k"&gt;QUEST&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;COMPLETE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"{player_name} finished the quest in {time} seconds"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in GDScript, you call it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"{player_name} finished the quest in {time} seconds"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="n"&gt;player_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;weapon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elapsed&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;Godot also supports C-style format strings (&lt;code&gt;%s&lt;/code&gt;, &lt;code&gt;%d&lt;/code&gt;) and rich text tags like &lt;code&gt;[color=red]&lt;/code&gt; in its RichTextLabel nodes. These are all over a typical game's translation file, and they all need to survive translation intact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where translations break
&lt;/h2&gt;

&lt;p&gt;The problem is not the translation itself. It is the variable tags inside the strings.&lt;/p&gt;

&lt;p&gt;When you paste &lt;code&gt;{player_name} picked up the {weapon}&lt;/code&gt; into a translation tool or general-purpose AI, several things can go wrong:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Curly braces get translated.&lt;/strong&gt; Some tools interpret &lt;code&gt;{player_name}&lt;/code&gt; as natural language and translate "player" and "name" into the target language. Your code calls &lt;code&gt;.format()&lt;/code&gt; with the English key. It finds no match. The placeholder renders as raw text, or your game crashes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Format specifiers get reordered.&lt;/strong&gt; &lt;code&gt;%s killed %d enemies&lt;/code&gt; depends on argument order. A translator might flip the sentence structure for grammar but leave &lt;code&gt;%s&lt;/code&gt; and &lt;code&gt;%d&lt;/code&gt; in the original positions, swapping which value fills which slot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rich text tags get mangled.&lt;/strong&gt; &lt;code&gt;[color=red]Warning[/color]&lt;/code&gt; might become &lt;code&gt;[couleur=rouge]Avertissement[/couleur]&lt;/code&gt; in French output, breaking BBCode parsing entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whitespace around tags disappears.&lt;/strong&gt; Tokenizers strip or merge spaces near braces and brackets, producing &lt;code&gt;{player_name}picked up&lt;/code&gt; with no space between the variable and the next word.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These bugs are silent. The CSV file looks fine. The game launches. Then a Japanese player sees &lt;code&gt;{player_name}&lt;/code&gt; rendered as literal text on screen because the translator converted the curly braces to full-width characters.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to protect your tags
&lt;/h2&gt;

&lt;p&gt;The reliable approach is to replace every variable tag with a placeholder token before translation, then restore the originals after.&lt;/p&gt;

&lt;p&gt;Here is the process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Scan each string for variable patterns.&lt;/strong&gt; Match &lt;code&gt;{...}&lt;/code&gt;, &lt;code&gt;%s&lt;/code&gt;, &lt;code&gt;%d&lt;/code&gt;, &lt;code&gt;%f&lt;/code&gt;, &lt;code&gt;[color=...]...[/color]&lt;/code&gt;, and any custom tags your project uses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replace each match with a numbered token&lt;/strong&gt; like &lt;code&gt;__VAR_0__&lt;/code&gt;, &lt;code&gt;__VAR_1__&lt;/code&gt;. Store the mapping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send only the tokenized string to translation.&lt;/strong&gt; The translator (human or AI) sees plain text with inert tokens it has no reason to modify.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;After translation, swap the tokens back&lt;/strong&gt; to the original variable tags, byte for byte.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the approach &lt;a href="//www.locafileai.com"&gt;LocaFile AI&lt;/a&gt; uses. When you upload a Godot CSV or .po file, it detects and locks every variable pattern, including &lt;code&gt;{player_name}&lt;/code&gt;, &lt;code&gt;%s&lt;/code&gt;, &lt;code&gt;%d&lt;/code&gt;, and &lt;code&gt;&amp;lt;color=red&amp;gt;&lt;/code&gt; tags, with regex-based token replacement before the AI model sees the string. After translation, the original tags are restored exactly. The AI only ever processes plain text with inert placeholders.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical checks before you ship
&lt;/h2&gt;

&lt;p&gt;Even with tags protected, verify your translations before release:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Test with pseudolocalization.&lt;/strong&gt; Godot has a built-in pseudolocalization mode (Project Settings &amp;gt; Internationalization &amp;gt; Locale) that replaces your strings with accented, elongated versions. It catches hardcoded strings you forgot to externalize and UI elements that overflow with longer text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check font coverage.&lt;/strong&gt; Godot's default font only covers Latin-1 characters. For CJK, Cyrillic, Arabic, or Thai, load a font like Noto Sans as a DynamicFont resource and set it as your theme's default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handle pluralization properly.&lt;/strong&gt; English has two plural forms. Polish has four. Arabic has six. Godot's &lt;code&gt;tr_n()&lt;/code&gt; function handles this per-locale, but your source strings need to be structured for it: &lt;code&gt;tr_n("There is %d apple", "There are %d apples", count)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test RTL languages.&lt;/strong&gt; If you are shipping in Arabic or Hebrew, Godot automatically mirrors UI anchors, margins, and control order. But icons with directional arrows (back/forward buttons) need manual attention.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The real cost of skipping localization
&lt;/h2&gt;

&lt;p&gt;Steam's audience is global. Roughly 60% of Steam users have their client set to a non-English language. Shipping English-only means your store page, your reviews, and your in-game text are invisible to most of the platform.&lt;/p&gt;

&lt;p&gt;The traditional path, hiring an agency, costs thousands of dollars per language and takes weeks. The shortcut of running your CSV through ChatGPT produces translations that look correct until &lt;code&gt;{player_name}&lt;/code&gt; shows up as literal text on a player's screen.&lt;/p&gt;

&lt;p&gt;Variable-safe translation closes that gap. Protect the tags, translate the text, verify with pseudolocalization, ship.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>programming</category>
      <category>software</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
