DEV Community

Cover image for Transifex now pays for 75% of our Claude Code subscription
Karan Mali
Karan Mali

Posted on

Transifex now pays for 75% of our Claude Code subscription

The $150 problem that was really a time problem

We were paying Transifex $150 a month, and the money wasn't even the part that hurt.

The real cost was time. Here's how a translation actually shipped. A developer adds a new string to the website, a button or a notification or an error message, in English. It goes to the dev branch. From there it shows up on the Transifex dashboard, sitting in a list of strings marked as "needs translation." Then someone from the business side, an Arabic speaker, goes in and translates it. The Transifex bot picks up those translations and injects them back into our repo through a GitHub PR. Only then does the Arabic version ship.

Count the handoffs. Developer → dashboard → business person → bot → repo. And here's the part that made it unavoidable: the product ships in both English and Arabic, but the engineering team speaks English. None of us could write or verify the Arabic ourselves, so every Arabic string was blocked on someone outside the team. If you shipped a big batch of strings, that person needed time to work through all of them.

In practice this took 3 to 4 days. For a one-word button label. Not because translating one word is hard, but because the word had to travel through a paid SaaS, a dashboard, a person, and a bot before it landed.

The thing about Transifex is that it worked. The human-in-the-loop part was genuinely good. You don't want to ship Arabic to real users without someone who speaks it signing off. But we were renting a $150/month round-trip to get a checkpoint we could have run ourselves, and paying for it in days of latency on every change.

Why we never fixed it before

The obvious question is: if the round-trip was that painful, why did we pay for it for years?

Because building your own translation pipeline used to be a bad trade. Think about what it would have taken pre-AI. You'd write code to parse the XLF files, figure out the placeholder syntax, handle the dynamic values, and then you'd still need a human who reads Arabic to do the translating. You haven't removed the slow part. You've just added a pile of parsing code to maintain on top of it. For a 4-person team with exactly two languages, English and Arabic, that math never closes. You shrug and pay the $150.

Quick context on those XLF files, because they matter later. XLF (short for XLIFF) is just an XML format for translations: a long list of units, each one pairing an English source with its Arabic target.

<trans-unit id="save_btn">
  <source>Save</source>
  <target>حفظ</target>
</trans-unit>
Enter fullscreen mode Exit fullscreen mode

Our frontend build tool generates the source side automatically from the app, and something fills in the target, a human or, now, the agent. Clean and boring. That held right up until the strings stopped being plain words.

This was never a hard problem, and that's the point. There aren't a hundred people doing our translations. It isn't some massive localization operation with thirty locales and a compliance process. It's en and ar. The effort to fix it just never dropped below the effort to live with it.

AI changed that calculation. Not because it can translate. Transifex's human translators could already do that fine. It changed it because an agent could read the XLF, understand the placeholder syntax, learn what each dynamic value meant, and draft the Arabic itself. The part that used to need a parser and a person now needed an agent and a person to check it. The effort to own the pipeline collapsed.

So I built it on a Saturday, as a side project. It didn't touch the roadmap or block anyone's work, and if it had flopped I'd have lost an afternoon. That's the whole reason it was worth doing. The payoff wasn't huge. AI just made the cost of trying almost nothing.

First cut, teaching an agent to read the files

My first version was naive, and I knew it would be. Point an agent at the Arabic translation file, tell it "translate the missing strings into Kuwaiti Arabic," let it run. The goal of that first pass wasn't to get it right. It was to see where it would fall over.

Simple strings were fine. A button that says "Save" becomes "حفظ" and we're done. The agent handled those immediately.

The trouble started with strings that have values baked into them. A notification doesn't say "Lease expires soon." It says "Lease A-102 (Marina Tower) expires in 14 days." The lease ID, the property name, the number of days: none of that is part of the translation. It gets dropped in later, when the notification goes out. In the file, all you see is numbered blanks. No names, no hints, just placeholders waiting to be filled.

That's where it broke. The agent had no way to know what each blank was for. Is the first one the lease ID, the tenant's name, or the date? You can't tell from the file, and if you don't know what a blank holds, you can't place it correctly in the sentence.

In Arabic that matters more than it sounds, because Arabic reads right-to-left and you don't mirror the English word order, you rebuild the sentence. Something that sat at the end in English often moves near the front in Arabic. So the agent couldn't swap words in place. It had to follow the sentence well enough to know where each piece lands once everything is rearranged.

The first cut got this wrong in all the ways you'd expect. It lost blanks, put them in the wrong spot, and once tried to "translate" one that was code, not words. A missing blank isn't a harmless typo. The notification ships with an empty gap where the lease ID should be: it looks perfectly translated and is quietly broken.

So the real problem was never "translate English to Arabic." The agent could do that in its sleep. It was "rebuild the sentence in Arabic without losing the moving parts inside it."

Give it the context, then make it prove the work

The fix came in two parts: give the agent the missing context, then stop trusting it to be careful.

The context part was simple once I understood the real problem. The reason the agent couldn't place those values was that the translation file didn't tell it what they were. But the original code did. Somewhere in the source there's a line that builds that notification, something like "Lease {leaseId} ({name}) expires in {days} days", with real, named variables. So the rule became: before you translate a string with blanks in it, go open the source file and read the line. Learn that the first blank is the lease ID and the second is the property name. Then translate. Once the agent was reading the source instead of guessing from the file, it started putting values where they belonged.

The second part was trusting nothing. I'd seen the agent ship strings that looked translated and were quietly broken, so "looks done" couldn't be the bar. I wrote a small validation script that checks every translated string mechanically: does it still have every value the English version had, none added, none dropped, none mangled? The agent isn't done when it thinks it's done. It's done when that check passes. If a value went missing, the script fails, and the agent has to go fix it and run again.

On top of that I wrote down the rules it kept getting wrong, once, in a spec file: restructure the sentence for Arabic instead of mirroring English, never touch the placeholders, keep numbers in the format the rest of the app uses. Instead of correcting the same mistakes by hand every time, the agent reads the rules and the checker enforces them.

That's the part I'd underline for anyone building with agents. The model doing the creative work, the translation itself, was never the hard part. The hard part was everything around it: feeding it the context it was missing, and bolting on a dumb, strict check at the end that it can't talk its way past. The clever translation was the easy 80%. The boring check is the 20% that made it something I'd ship.

You still can't trust it blindly

Even with the context and the checker, I wasn't going to point this at real users and walk away. The validator proves the placeholders are intact. It can't tell you whether the Arabic reads well, or whether the agent picked a slightly-off word for something. For that you need a person who speaks the language.

That was the one genuinely good thing about Transifex: the human checkpoint. A native Arabic speaker on the business side looked at every translation before it shipped. I didn't want to lose that. I wanted to keep the exact same checkpoint and just stop paying $150 a month for the privilege.

The problem is where the agent's translations live. They're in files, in a git repo. A business reviewer isn't going to clone a repo and edit XML, and honestly I don't want them anywhere near it. One stray character and the build breaks. So the agent produces the Arabic, but a non-developer has no safe way to review or fix it.

So I built a small web app for that, a translation portal. It's just a table. English on one side, the agent's Arabic on the other, grouped into sections like Invoices or Auth so it's not one giant wall of strings. The reviewer reads down the list, fixes anything that's off, and saves. No git, no XML, no way to break anything. Just the part of the job they're good at.

There's a subtle problem hiding in that table. The agent writes Arabic into the same files a human later edits, and both commit to the same repo. So once a string is in there, you can't tell by looking whether a person signed off on it or whether it's raw machine output nobody has checked yet. Unreviewed AI text and human-approved text look identical. If the human check was going to mean anything, I needed to know which strings a person had signed off on and which were still the agent's raw guess.

The simple move was to keep that approval state completely out of the translation files. The files stay pure: the agent commits them, the apps build from them, nothing else lives in them. I looked at marking the files themselves, but XLIFF has a built-in "reviewed" flag that our frontend build tool wipes every time the agent regenerates the file, and the mobile JSON has nowhere to put per-string metadata without breaking the app that loads it. So the approval record lives somewhere separate, in blob storage the portal owns. It's just a ledger. For each string: the exact Arabic a human approved, who approved it, and when.

The part I like is that I don't store a status anywhere. I derive it. When the portal loads, it diffs the live translation against the ledger. If a string isn't in the ledger, nobody has approved it, so it's in the review queue. If it's in the ledger but the current text doesn't match what was approved, it changed since, so it goes back in the queue. Only an exact match counts as approved. Which means when the agent rewrites a string, it stops matching the approved snapshot and falls back into needs-review on its own. The agent doesn't know the review system exists, and it doesn't need to.

Two homes: translation files live in the git repo, the approval ledger lives in blob storage, and the portal derives each string's review status by diffing the live text against the approved snapshot.
Translations stay in the repo. Approval state lives on its own. Status is computed, never stored in the files.

That's the whole shape of it. The agent drafts fast. The validator catches anything structurally broken. Then a human who speaks the language signs off, in a screen built for exactly that and nothing else. Same human-in-the-loop Transifex gave us. We just own all three pieces now, and pay for none of them.

Closing the loop

The loop is closed, and it runs without me in it. When a reviewer approves an edit in the portal, the portal commits that change straight back to the app repo through the GitHub API. Our existing deploy pipelines see the commit and ship it exactly like any other change. No dashboard, no third-party bot, no exporting files and pasting them in by hand. The reviewer hits approve, and the Arabic is on its way to production.

Two things were fiddlier than I expected. First, the obvious way to write a file through GitHub's API, the simple "update this file" endpoint, quietly falls over on big files, and our web translation file is over a megabyte. So I had to drop down to the lower-level API that commits the way git does under the hood: build the file, build the commit, move the branch pointer. More steps, but it doesn't choke on size.

The second one bit me harder, and it's the mistake I'd warn anyone else about. My first version, whenever a reviewer saved, just rewrote the entire Arabic file from whatever the editor was holding in memory. That seems completely fine: the editor has every string loaded, so writing them all back out should be lossless. It wasn't. A one-word fix came out as a commit that touched the whole file, so every line showed up as changed and you couldn't see what had changed. Worse, if someone had edited a different string in the meantime, my whole-file write would quietly stomp it flat. The portal only knew about the version it had loaded, and it wrote that version over the top of everything.

It also quietly lost things. The editor built its list from the English file, so anything that existed only in Arabic was invisible to it and got erased on save. The clearest example: a label as ordinary as Mr. would break the save. My code used dots to work out the file's nesting, so it read the dot inside Mr. as if it meant "go one level deeper" and mangled the structure. Arabic plural forms that English doesn't have vanished the same way, because nothing in the English-built list knew they existed.

The fix was to stop rewriting the file at all. Now the portal reads the real Arabic file, changes only the exact values the reviewer touched, leaves every other byte alone, and commits that. A one-word fix is a one-line commit. Nothing else moves, nothing gets dropped, and two people editing different strings don't overwrite each other.

The other surprise was how far the same table stretched. It started as one screen for the web app and the two mobile apps. It now feeds six surfaces across two separate repos and three different translation formats: the web app's XLIFF, the mobile apps' JSON, our listing site and its admin panel in another flavor of JSON, and a React Native app whose strings live as plain objects inside a TypeScript file. The reviewer sees the same table every time. All the format-specific mess stays behind the API, where each surface knows how to read and write its own files.

Where it sits right now: this is all running on a branch, not merged to main yet. But the loop is closed. And the difference is the whole point of the project. Under Transifex a new string took 3 to 4 days to ship in Arabic: dashboard, person, bot, repo. Now the agent drafts it in one pass, the reviewer checks it in the portal, they hit save, and it's a commit on the repo. Days down to hours.

Before: a five-hop round-trip through Transifex taking 3 to 4 days per string. After: agent drafts, human approves in the portal, auto-commit to the repo, in hours.
Same checkpoint, minus the dashboard and the bot. The human sign-off stayed; the waiting didn't.

And it costs nothing to host. The portal is a small Next.js app on Vercel, the approval ledger lives in Vercel's blob storage, and both sit inside the free tier. So the $150 a month didn't move somewhere else, it went to zero. If anything, the money we stopped sending Transifex now covers about three-quarters of the Claude Code subscription the agent runs on. The tool we cancelled is paying for the tool that replaced it.

When this is the right call, and when it isn't

This isn't a case for everyone firing Transifex and building their own. For us it was right. For a lot of teams it would be a mistake, and the line between the two matters.

It worked for us because the dimension that decides this stayed small. The portal feeds six app surfaces now, but it's still two languages. One dialect of Arabic. One product domain the agent could learn: tenants, leases, listings, invoices. A handful of people, none of whom needed a fancy translation-management UI with roles and workflows and glossaries. When the whole problem fits in your head, owning it is cheap, and a tool built for a hundred-language operation is mostly paying for problems you don't have.

Flip any of those and I'd tell you to keep paying. Thirty languages, and now you're maintaining an agent and a review flow per language and you've built a localization company by accident. Legal or medical copy where a bad translation is a liability, and you want a vendor with a real process and someone to blame. A real localization team who live in translation-memory and termbases all day, and taking their tools away just makes their job worse to save a subscription.

The real thing I took from this isn't about translation. It's that AI quietly moved a line I'd stopped checking. "Just pay for it" is the right default for any problem that's annoying but not worth a week of engineering, and that default is built on what building it used to cost. That cost dropped. Some of the things you're renting out of habit are now a Saturday. Not most of them. But more than there used to be, and the only way to find out which is to try one instead of assuming the old answer still holds.

Links & tools

Top comments (1)

Collapse
 
atharv_dange_357d79caedc9 profile image
Atharv Dange

Really enjoyed reading this. The amount of thought that went into the validation and review flow is what stood out to me. It's easy to get an agent to generate translations; making the output reliable enough to ship is the interesting part. Great write-up