Your Portuguese is always three releases behind your English
Localization fails because it ships as a one-time pass and the source keeps moving, the fix is a loop that re-fires every translation the moment the English string it came from changes.
Apollo Space Research
Apollo Space
Open the English version of almost any product and the buttons make sense. Switch to Portuguese, or Spanish, or Japanese, and somewhere on the third screen a word is wrong. Not mistranslated, stale. The English says “Archive,” the Portuguese still says “Delete,” because three releases ago the button changed and the translation didn’t. Nobody decided to ship a lie. The lie shipped because the translation was finished once, and the product wasn’t.
That gap between “finished once” and “never finished” is the whole problem with localization. And it’s the thing almost every tool gets backwards.
A translation is not a deliverable. It’s a relationship to a string that keeps changing.
This post is about what changes when you treat localization that way, not as a job you complete, but as a loop you maintain. The mechanism is small. The consequence is that the Portuguese stops lagging the English.
The naive version: a localization is a pass
Here’s how it’s done almost everywhere, and it’s reasonable, which is why it’s a trap.
You finish a feature. The English strings settle. You export them, a file of keys and values, and you send the file off to be translated, by a vendor, by a contractor, by a script that calls a model. A few days later a translated file comes back. You drop it in. Every language lights up. You ship. Localization: done.
It is done. That’s exactly the problem. It’s done the way a photograph is done, a perfect record of one moment that starts aging the instant it’s taken.
Because the English doesn’t hold still. Next sprint someone renames “Archive” to “Move to archive.” Someone splits one screen into two and writes four new strings. Someone fixes a typo in the source and, without thinking, changes the meaning slightly. Each of those edits is invisible to the translated files, which are sitting in the repository exactly as they came back from the vendor, frozen at the moment of the pass. The English moved on. The Portuguese stayed where it was photographed.
So the gap opens. Quietly, one string at a time. A new button shows up untranslated, at best it falls back to English, at worst it shows a raw key like button.archive. A renamed string keeps its old translation, now subtly wrong. A deleted string leaves a dead entry nobody cleans up. None of it is loud enough to fail a build. All of it is loud enough for a customer in São Paulo to notice that the product was clearly built for someone else.
The naive version isn’t wrong because the translations are bad. It’s wrong because it treats a moving target as a still one. You can run the pass again, of course, re-export, re-send, re-import. But you only do that when someone notices the drift, which means the drift has already shipped. The pass is always a response to a problem the customer found first.
Why “just re-translate everything” doesn’t fix it
The obvious patch is to make the pass cheap enough to run constantly. Translation by a model costs a fraction of what a human vendor charges, so why not re-translate the entire string catalog on every release and call the drift solved?
Two reasons, and they’re the reasons that turn the cheap idea expensive.
The first is that re-translating everything throws away work you wanted to keep. Some of your strings were translated once, then hand-corrected by someone who actually speaks the language and knew that the literal translation read like a robot. A blanket re-run overwrites those corrections with fresh machine output. You don’t get fewer errors. You get the same errors back, plus the loss of every fix a human made. The system that was supposed to maintain quality just erased it.
The second is that translation without context produces confident nonsense. The word “Open” is a verb on a button and an adjective on a status badge, “Open ticket” versus “Ticket is open.” A model handed a flat list of strings has no idea which is which, so it guesses, and it guesses the same way every time regardless of where the string lives. “Save” near a document means one thing; “Save” near a checkout means another; “Save 20%” means a third. Re-translate the whole catalog blindly and you launder good translations and bad ones together at industrial speed.
So the cheap fix fails twice. It’s wasteful, most strings didn’t change and didn’t need touching. And it’s careless, it strips the context that made the good translations good. Doing the wrong thing faster is not the same as doing the right thing.
The lesson hiding in both failures is the same one. The unit of localization was never the file. It was the string, and specifically, the string’s relationship to the source it came from. A translation is not a deliverable. It’s a relationship to a string that keeps changing. Fix the wrong unit and you’ll either ship stale work or destroy good work, and usually both.
Our way: localize the change, not the catalog
Here’s the idea, and like most good ideas it’s almost annoyingly simple once you see it. Stop translating catalogs. Start translating changes.
A source string is not a value in a file. It’s a value with a history, it was added on some date, last edited on another, and it carries a fingerprint of its current English text. The translation that goes with it remembers which version of the English it was made from. So the system can always answer one question that the file-based world can’t: is this translation still describing the English it was born from, or has the English moved underneath it?
That single question is the whole engine. When the English changes, its fingerprint changes, and every translation that was made from the old fingerprint is now, provably, out of date, not by guess, by record. The system doesn’t re-translate the catalog. It re-fires exactly the strings whose source moved, and leaves the thousands that didn’t completely alone.
And it re-fires them with context, because by now it knows where each string lives. This string is a button label; that one is an error message; this one sits next to a price. A company brain that has read the product knows the difference between the imperative “Open” and the adjective “open,” because it knows the screen each one belongs to. So the re-translation isn’t a blind guess against a flat list, it’s a context-aware rewrite of one string, the string that actually changed, into the voice the rest of that surface already uses.
Walk one edit through it. Imagine a designer renames “Archive” to “Move to archive” on a Tuesday afternoon. The English string’s fingerprint changes the moment that edit lands. The system notices that every language pointing at the old fingerprint is now stale, say a dozen of them, and re-fires only those, each one re-translated with the knowledge that this is a button that performs an action on the current item. The human reviewer, instead of facing a full re-export of the entire catalog, sees a tidy list: one source string changed, here are the dozen translations it produced, approve or correct. By the time the renamed button reaches a customer, the Portuguese already says the Portuguese for “Move to archive,” not the Portuguese for the button that no longer exists.
The part that makes it maintenance, not magic: a human still signs
It would be easy to read all this as “the agent translates everything automatically and you never look again.” That’s not the design, and the place it would break tells you why.
Machine translation is good enough to draft and not good enough to trust unattended on the strings that carry weight. A legal disclaimer, a refund-policy line, the exact wording of a consent checkbox, these are strings where a plausible-sounding mistranslation is worse than an obvious one, because nobody catches it until it matters. The naive auto-pilot ships those silently. The careful loop routes them.
So the re-fire doesn’t end at a translation. It ends at a proposal. The system did the tedious part, it found the strings that moved, drafted context-aware translations for each, and assembled them into one small, reviewable change. A person who reads the language gives the nod, or fixes the one that reads stiffly, and their fix is remembered, fingerprinted, and never silently overwritten by a future re-run. The cheap re-translate-everything approach couldn’t promise that. This loop is built so it can.
That’s the difference between automation that replaces the reviewer and automation that respects them. The work that’s tedious and mechanical, diffing the source, finding the stale strings, drafting the first pass, is exactly the work a human does badly and resents doing. The work that’s high-judgment, does this consent line say the right thing in this language and this culture, is exactly the work a human does well and a model shouldn’t do alone. Put the loop on the first and the person on the second, and you get speed without surrendering the part that actually needed a human.
What this buys, and what it stops costing
Add it up and the change is not “translations get done faster.” It’s that translations stop un-doing themselves.
In the old model, every release was a small bet that nobody changed a localized string, and you lost that bet quietly and often. The cost wasn’t the translation work, that was cheap. The cost was the trickle of stale strings reaching customers in every language but the one you build in, each one a tiny signal that the product wasn’t really made for them. Suppose a fast-moving team ships dozens of string edits a week; in the pass model, some fraction of those drift into every non-English locale and sit there until a customer or a support ticket surfaces them. The drift was never a translation problem. It was a maintenance problem wearing a translation costume.
A translation is not a deliverable. It’s a relationship to a string that keeps changing. Once you hold the relationship instead of the file, the question “is our Portuguese current?” stops being a thing you audit and starts being a thing the system can answer at any moment, because it knows which translations point at which source versions. Current isn’t a state you periodically restore. It’s the default the loop maintains.
And the cost it stops paying is the worst kind of cost, the silent one. No team ever put “let the Spanish go stale” on a roadmap. It happened in the gaps between intentions, one un-re-fired string at a time, until the second-language experience was visibly a generation behind the first. Closing that gap doesn’t require a heroic localization sprint. It requires the source change and the translation to be the same event, instead of two events with a widening distance between them.
The turn: who you stop asking to be the safety net
Strip away the fingerprints and the re-fires and there’s a person at the bottom of all this, and it’s worth saying who.
Somewhere in a company shipping in more than one language, someone is the human diff tool. They speak the second language, and so the job of catching the drift falls to them by default: noticing the new untranslated button, remembering that “Archive” got renamed, flagging the string that’s been wrong since the spring. It’s not in their job description. It became their job because the system wouldn’t do it, and someone had to, and they were the one who’d notice. Every hour they spend being a freshness checker for a string catalog is an hour they don’t spend on the thing only a fluent human can do, making the product sound like it was written in that language, not translated into it.
That’s the work worth protecting. Not the diffing. The voice. The judgment that a literal translation is technically correct and culturally tone-deaf. The knowledge that this phrase lands warm in one market and cold in another. A loop can find the strings that moved and draft the first version; it can never decide what “right” sounds like to a customer who grew up speaking the language. The point of automating the maintenance is not to remove the human from localization. It’s to move them off the part that was beneath them and onto the part that was always theirs.
That’s what we’re building toward at Apollo, not a faster translation button, but a localization loop where the source change and the translation are one event, so the second language never has to lag the first. If you’ve ever switched a product into your own language and felt, on the third screen, that it wasn’t really meant for you, you already know the gap this closes. The translation was never the hard part. Keeping it true was.
Apollo runs your company's repetitive ops so your team doesn't.
Join the waitlist for early access, founding-user pricing, and a front-row seat as we ship.
Join the waitlistThe slow death of a marketer's voice
You publish one real piece a week and quietly translate it into ten, and each translation is a tiny chance to sound a little less like yourself. We built the OS because nothing on the market was guarding that.
Product ThinkingThe day someone quits, your company forgets how it works
Onboarding isn't broken because training is bad. It's broken because your company can't remember, and we got tired of watching the answer walk out the door.
Product ThinkingThe first thing a new hire should do is read the company
A great onboarding doesn't hand you docs, it already knows who you are by the time you log in.