<?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: Rutvik Panchal</title>
    <description>The latest articles on DEV Community by Rutvik Panchal (@rutvik_panchal_b48b8efc56).</description>
    <link>https://dev.to/rutvik_panchal_b48b8efc56</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%2F3879181%2Fdf440e23-a2c0-4f49-a15c-7f7dc87e40a3.jpg</url>
      <title>DEV Community: Rutvik Panchal</title>
      <link>https://dev.to/rutvik_panchal_b48b8efc56</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rutvik_panchal_b48b8efc56"/>
    <language>en</language>
    <item>
      <title>Adding 70-language translation to an image API without paying per word</title>
      <dc:creator>Rutvik Panchal</dc:creator>
      <pubDate>Tue, 09 Jun 2026 16:03:04 +0000</pubDate>
      <link>https://dev.to/rutvik_panchal_b48b8efc56/adding-70-language-translation-to-an-image-api-without-paying-per-word-1o99</link>
      <guid>https://dev.to/rutvik_panchal_b48b8efc56/adding-70-language-translation-to-an-image-api-without-paying-per-word-1o99</guid>
      <description>&lt;p&gt;I run &lt;a href="https://pixeldrive.pro" rel="noopener noreferrer"&gt;PixelDrive&lt;/a&gt;, an API + editor that turns templates&lt;br&gt;
into branded images. You design once, mark layers as variables, then POST data&lt;br&gt;
and get a PNG. The most common request was localization: teams were making the&lt;br&gt;
same graphic in 10 languages by hand.&lt;/p&gt;

&lt;p&gt;Here's how I built translation into the render pipeline, and why I self-hosted&lt;br&gt;
it instead of calling a cloud translation API.&lt;/p&gt;
&lt;h2&gt;
  
  
  The shape of the problem
&lt;/h2&gt;

&lt;p&gt;Translation here is not "translate documents." It's &lt;strong&gt;short, repetitive design&lt;br&gt;
copy&lt;/strong&gt; (headlines, CTAs, labels) rendered onto images, on a server that's&lt;br&gt;
already CPU-bound doing the actual rendering. Two facts drove every decision:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The text is tiny and cacheable. Translate &lt;code&gt;"Spring Sale" -&amp;gt; es&lt;/code&gt; once, store
it, and every future use is a sub-millisecond cache hit.&lt;/li&gt;
&lt;li&gt;The box has no spare CPU and no GPU. Anything I run competes with the image
renderer.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Why self-host
&lt;/h2&gt;

&lt;p&gt;With caching, the per-word cost of a paid API basically disappears, so cost was&lt;br&gt;
not the deciding factor. The deciding factors were control and not wanting a&lt;br&gt;
network hop in the render path. The trade-off is you have to pick a model that&lt;br&gt;
is small, fast on CPU, and good on short copy.&lt;/p&gt;

&lt;p&gt;The sweet spot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model:&lt;/strong&gt; &lt;code&gt;facebook/nllb-200-distilled-1.3B&lt;/code&gt; (200 languages, purpose-built
for translation).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime:&lt;/strong&gt; &lt;a href="https://github.com/OpenNMT/CTranslate2" rel="noopener noreferrer"&gt;CTranslate2&lt;/a&gt; with int8
quantization. This is the key piece. It shrinks the model to ~1.3 GB and runs
CPU inference fast. Do &lt;strong&gt;not&lt;/strong&gt; run raw &lt;code&gt;transformers&lt;/code&gt; on CPU for this.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A multi-stage Docker build does the conversion once, so the runtime image&lt;br&gt;
carries no torch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# stage 1: convert + quantize (needs torch, thrown away)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;python:3.11-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;converter&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;ctranslate2 &lt;span class="s2"&gt;"transformers[sentencepiece]"&lt;/span&gt; torch &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--extra-index-url&lt;/span&gt; https://download.pytorch.org/whl/cpu
&lt;span class="k"&gt;RUN &lt;/span&gt;ct2-transformers-converter &lt;span class="se"&gt;\
&lt;/span&gt;      &lt;span class="nt"&gt;--model&lt;/span&gt; facebook/nllb-200-distilled-1.3B &lt;span class="se"&gt;\
&lt;/span&gt;      &lt;span class="nt"&gt;--quantization&lt;/span&gt; int8 &lt;span class="nt"&gt;--output_dir&lt;/span&gt; /models/nllb-ct2

&lt;span class="c"&gt;# stage 2: runtime (ctranslate2 + tokenizer only)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.11-slim&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;ctranslate2 &lt;span class="s2"&gt;"transformers[sentencepiece]"&lt;/span&gt; fastapi &lt;span class="s2"&gt;"uvicorn[standard]"&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=converter /models /models&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service itself is a tiny FastAPI app with &lt;code&gt;/translate&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;/translate_batch&lt;/code&gt;. NLLB uses FLORES-200 codes (&lt;code&gt;spa_Latn&lt;/code&gt;), so I map ISO codes&lt;br&gt;
(&lt;code&gt;es&lt;/code&gt;) and normalize typographic punctuation (em dashes, curly quotes) that the&lt;br&gt;
tokenizer would otherwise drop as &lt;code&gt;&amp;lt;unk&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it into rendering
&lt;/h2&gt;

&lt;p&gt;The render service is the only place that needs translation, and it already has&lt;br&gt;
a Redis/Kvrocks cache. The flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A text field can carry a language: &lt;code&gt;{ "headline": { "text": "Spring Sale", "lang": "es" } }&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The render cache key already includes the full payload (so &lt;code&gt;es&lt;/code&gt; and &lt;code&gt;fr&lt;/code&gt; are
different cache entries). On a &lt;strong&gt;cache miss only&lt;/strong&gt;, translate.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;translatePayload()&lt;/code&gt; collects every text value with a &lt;code&gt;lang&lt;/code&gt;, checks the
cache (&lt;code&gt;tr:&amp;lt;lang&amp;gt;:&amp;lt;sha(text)&amp;gt;&lt;/code&gt;), batches the misses to the translation
service, writes results back to the cache, and swaps the text in.&lt;/li&gt;
&lt;li&gt;The harness draws the translated text. The same cache key is used by the
editor, so a translation done in the editor is reused at render time and
vice versa.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two operational details that mattered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The translation container is &lt;strong&gt;CPU-capped&lt;/strong&gt; (a couple of cores). Even a burst
of cache misses can't starve the renderer.&lt;/li&gt;
&lt;li&gt;It's &lt;strong&gt;best-effort&lt;/strong&gt;: if the service is unavailable, it falls back to the
original text. Translation never breaks a render.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/v&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;/render&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;"templateId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"payload"&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;"headline"&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;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Welcome to our spring sale"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"lang"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"es"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;renders &lt;strong&gt;"Bienvenido a nuestra venta de primavera"&lt;/strong&gt; onto the image. In the&lt;br&gt;
editor there's a one-click "translate the whole template" button with a&lt;br&gt;
searchable 70-language picker, and the bulk-CSV flow supports a per-row &lt;code&gt;lang&lt;/code&gt;&lt;br&gt;
column so a single upload can render a batch across markets.&lt;/p&gt;

&lt;p&gt;Because every &lt;code&gt;(text, language)&lt;/code&gt; pair is cached forever, the model only runs on&lt;br&gt;
genuinely new strings, which for marketing copy is rare after warmup. A small&lt;br&gt;
CPU model plus aggressive caching turned out to be a better fit than a cloud API&lt;br&gt;
for this particular shape of problem.&lt;/p&gt;

&lt;p&gt;If you want to see it in action: &lt;a href="https://pixeldrive.pro" rel="noopener noreferrer"&gt;pixeldrive.pro&lt;/a&gt;.&lt;br&gt;
Happy to answer questions about the setup in the comments.&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>ai</category>
      <category>machinelearning</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building an Open-Source Polotno Alternative (Canva-like Editor)</title>
      <dc:creator>Rutvik Panchal</dc:creator>
      <pubDate>Tue, 14 Apr 2026 18:48:45 +0000</pubDate>
      <link>https://dev.to/rutvik_panchal_b48b8efc56/building-an-open-source-polotno-alternative-canva-like-editor-53pp</link>
      <guid>https://dev.to/rutvik_panchal_b48b8efc56/building-an-open-source-polotno-alternative-canva-like-editor-53pp</guid>
      <description>&lt;p&gt;In the past few weeks, I built an open-source design editor that works similarly to Polotno — and decided to open-source it as OpenPolotno.&lt;/p&gt;

&lt;p&gt;The goal was simple: create a Canva-like editor that developers can fully control, customize, and self-host.&lt;/p&gt;

&lt;p&gt;🚀 Why I Built This&lt;/p&gt;

&lt;p&gt;Polotno is a great tool, especially for quickly building design editors.&lt;/p&gt;

&lt;p&gt;But while working with it, I felt a few limitations:&lt;/p&gt;

&lt;p&gt;Limited control over customization&lt;br&gt;
Dependency on a proprietary ecosystem&lt;br&gt;
Constraints when scaling or extending features&lt;/p&gt;

&lt;p&gt;I wanted something that:&lt;/p&gt;

&lt;p&gt;Gives full ownership of the editor&lt;br&gt;
Allows deep customization&lt;br&gt;
Can be self-hosted without restrictions&lt;/p&gt;

&lt;p&gt;So I decided to build an alternative — compatible with Polotno, but fully open.&lt;/p&gt;

&lt;p&gt;🧠 The Idea&lt;/p&gt;

&lt;p&gt;Instead of reinventing everything, I focused on:&lt;/p&gt;

&lt;p&gt;👉 Compatibility&lt;/p&gt;

&lt;p&gt;If developers are already using Polotno, they shouldn’t need to rewrite everything.&lt;/p&gt;

&lt;p&gt;So OpenPolotno supports:&lt;/p&gt;

&lt;p&gt;Similar API structure&lt;br&gt;
Template compatibility&lt;br&gt;
Familiar data format&lt;/p&gt;

&lt;p&gt;This makes migration or integration much easier.&lt;/p&gt;

&lt;p&gt;✨ Features&lt;/p&gt;

&lt;p&gt;Here’s what the editor currently supports:&lt;/p&gt;

&lt;p&gt;🎨 Drag-and-drop canvas editor&lt;br&gt;
🖼️ Text, images, and layer management&lt;br&gt;
🧩 Polotno-compatible templates&lt;br&gt;
🔌 Similar API structure&lt;br&gt;
💾 JSON-based import/export&lt;br&gt;
⚡ Fast and lightweight performance&lt;br&gt;
🔓 Fully open-source&lt;br&gt;
🧱 Challenges I Faced&lt;/p&gt;

&lt;p&gt;Building a design editor is not trivial. Some of the biggest challenges were:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Canvas Rendering Performance&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Handling multiple elements (text, images, layers) while keeping the editor smooth required careful optimization.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Data Structure Compatibility&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Making the system compatible with Polotno templates while keeping it flexible internally was tricky.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;UX Complexity&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Even small features (like dragging, resizing, layering) require a lot of edge-case handling.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Scalability&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Designing the architecture so it can grow into a full product (plugins, extensions, etc.) took planning.&lt;/p&gt;

&lt;p&gt;🎯 What This Can Be Used For&lt;/p&gt;

&lt;p&gt;OpenPolotno can be used to build:&lt;/p&gt;

&lt;p&gt;Canva-like SaaS products&lt;br&gt;
Social media design tools&lt;br&gt;
Internal design platforms&lt;br&gt;
Marketing automation tools&lt;/p&gt;

&lt;p&gt;Basically, anything that requires a visual editor.&lt;/p&gt;

&lt;p&gt;🔗 Links&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/therutvikp/OpenPolotno" rel="noopener noreferrer"&gt;https://github.com/therutvikp/OpenPolotno&lt;/a&gt;&lt;br&gt;
NPM: &lt;a href="https://www.npmjs.com/package/openpolotno" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/openpolotno&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;💬 Feedback Welcome&lt;/p&gt;

&lt;p&gt;This is just the beginning. I’d love feedback from developers who:&lt;/p&gt;

&lt;p&gt;Have used Polotno&lt;br&gt;
Are building design tools&lt;br&gt;
Need a customizable editor&lt;/p&gt;

&lt;p&gt;If you have suggestions or ideas, feel free to share 🙌&lt;/p&gt;

</description>
      <category>design</category>
      <category>opensource</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
