Back in February I had EF-Map translated into Ukrainian, Russian, Simplified Chinese and Filipino. It was machine translated first, then a handful of native speakers went through a Google Sheet and corrected the parts that read badly. That worked well. There were not many corrections, which surprised me at the time.
Then I carried on building. Six months of it. The arcade games, Gate Activity, the Economy panel, Query Console, Explorer Cards, Shared Storage, the Killboard replay controls, Discord Presence, the whole Yeet Planner. And every single one of those shipped with its text written directly into the code in English.
That is the part I did not appreciate until this week. If you had EF-Map set to Russian, the newer half of the app was not badly translated. It was not translated at all, because there was nothing there for a translator to translate. The strings had never been extracted. There was no key, no entry in a file, nothing for anyone to correct even if they had wanted to. It was invisible, and it stayed invisible for six months because I never went back and looked.
The prompt that missed
I kicked this off late one evening intending it to be an overnight job. What I asked for, roughly, was to translate the stuff that was not translated, and to add German while it was at it.
The agent did exactly that, and it was not what I wanted.
It found that each language file had 776 of the 915 keys that existed, filled in the missing 139, added German across all 915, verified nothing existing had been changed, and deployed a preview. It was good work. It was thorough. It measured the width of the buttons the text sits in so nothing would clip. And it came back far quicker than an overnight job should, which is what made me look.
I was still awake, which was lucky. I opened the preview, switched to German, and the panels were still mostly English. So I went back and explained it properly. I did not want the translation file topped up. I wanted the parts of the app that had never been through the translation process at all to go through it, and if that meant writing code to make those strings translatable in the first place, then that was the job.
That reading was available in what I originally said, if you already knew the answer. It was not available if you did not. The agent had solved the problem I described rather than the problem I had, and the two only diverged because I knew something about my own codebase that I had not put into words. I have done this enough times now that I should have caught it, and I still did not.
It finished too fast. Not wrong, not broken, just quicker than the shape of the job suggested. That is worth paying attention to. When an agent hands back early on something you expected to take hours, the most likely explanation is not that it was efficient, it is that you and it are solving different problems.
Measuring it properly
The second attempt started by counting, which is what the first one should have done.
Rather than grep for quoted strings, which produces mountains of false positives, it walked the TypeScript compiler's own syntax tree across all 205 components and pulled out text that a user actually sees. Text between tags, and the attributes that get read aloud or hovered, like titles, placeholders and labels. That found 6,592 hardcoded strings.
Then it cross-referenced against git, and the picture matched my memory exactly. 139 component files had been created since the February baseline. The new stuff really was the problem.
Not all 6,592 were worth doing. Broken down by where they live:
| Surface | Strings |
|---|---|
| App UI, the panels people actually click | 2,877 |
| Help documentation prose | 2,114 |
| Embed guide, developer docs | 472 |
| Operator-only tools I am the only user of | 472 |
| Stats dashboard | 344 |
| Static pages | 235 |
The help documentation is one file, 224 KB of English prose. Translating that fivefold adds roughly a megabyte of locale data for the least-visited surface in the app, so I left it. Everything else in the interactive app got done.
The safety net that made it possible
Wiring 1,750 strings through a translation function means touching about 60 files. The way that goes wrong is subtle. You do not get a crash, you get a panel that silently renders routing.tab.p2p where a word should be, and you do not find out until a user tells you.
The trick used here is simple enough that I am slightly annoyed I had not thought of it. Every catalog entry is the original English string, character for character. So wiring a string up must be a no-op in English. If the rendered English changes at all, something broke.
So before touching anything, it captured every visible string across eight views, 1,308 of them, and captured them again afterwards. The only difference across the whole app was a countdown timer that had ticked over. Everything else was byte identical.
That converts a scary refactor into a verifiable one. It ran again after the second wave and was clean again.
On top of that, a static scan checked every single translation call in all 815 source files against the catalog. Zero missing keys. That is a stronger guarantee than clicking through the app, because it covers the panels nobody remembered to open.
What the languages actually needed
The bit I found genuinely interesting is that each language broke in a different place, and none of the breakages were vocabulary problems.
The live event ticker builds sentences from a template with optional pieces slotted in, something like {killer} destroyed {victim}{inSystem}. English is relaxed about where the optional bits go, because they trail after the verb. Nothing else is.
German puts the participle at the end of the clause, so the slots have to move inside the verb bracket, giving {killer} hat {victim}{inSystem} zerstört. Russian and Ukrainian cannot mark who did something after a passive participle when the name is an indeclinable Latin string like a player handle, so "anchored by X" had to become a parenthetical that governs no case at all. Chinese wants the agent and the location before the verb, not after it. Filipino is verb-initial and patient-focus, so the agent marker has to sit next to the verb, because Na-anchor ang Foo ni Bar reads as Bar's Foo rather than Bar doing the anchoring.
Every template was checked with the optional pieces both present and absent, because a sentence that only works when the optional part is missing is a bug that shows up at the worst possible moment.
There were two other classes of problem worth mentioning. The first is width. The tool rail labels sit in an 80 pixel box with hyphenation disabled, which means an over-long single word does not wrap, it clips mid-word. The fonts EF-Map ships only cover Latin, so Cyrillic and Chinese fall back to a system font that is noticeably wider, and every budget needed shaving for those. German ended up the only one of the six languages with no clipped labels anywhere, which I did not expect.
The second is that some strings carry a leading or trailing space on purpose, because they wrap around a bold word in the middle of a sentence. Lose the space and the sentence visibly runs together. Those got flagged and preserved, with two deliberate exceptions where the language genuinely needed the space moved.
A constellation in EF-Map is an administrative grouping of star systems, a volume of space. The obvious German word is Sternbild, and it is wrong, because a Sternbild is a pattern you see from Earth, a line of sight illusion. The correct word is Konstellation. That is exactly the kind of error a straight machine translation makes and a native speaker catches instantly, which is why the human pass still matters.
What it cost
Forty-five subagents ran over the course of the night. Research first, three of them building a dossier of where every string physically appears, a glossary extracted from the existing human-corrected translations so the new work would match, and a German style guide researched against CCP's own German EVE material. Then eight doing the code wiring in parallel, each owning its own files. Then thirty translating, chunked by feature area so each one saw a coherent slice of the app rather than an alphabetical list.
Looking at the commit timestamps, the first misdirected pass ran to about half past one in the morning. The corrected wiring landed just before two, the five languages were done by twenty to four, and the curation tool went in the following morning.
The result is that every language went from 776 keys to 2,665, German included, from nothing. That is roughly 10,200 individual translations, all of them written with the surrounding interface as context rather than as a list of disconnected phrases.
The cost is the part I keep turning over. This consumed about ten percent of one week on a Claude Max 20x subscription. That is roughly two complete weekly Claude Pro allowances, or about forty percent of a week on Max 5x. Going by the widely quoted estimate that fully using Max 20x is equivalent to something like eight thousand dollars a month at standard API rates, the same workload would have been somewhere around a hundred and eighty-five to two hundred dollars through the API.
I have no idea what a professional localisation house would charge for ten thousand strings across five languages with genuine context awareness, but it is not two hundred dollars, and it is not one night. I am not claiming the output matches a professional human translator, because it does not, which is the entire reason for the next step. But as a starting point that native speakers can correct rather than redo, the economics are hard to argue with.
The correction step, and why it is not a spreadsheet this time
Last time the human pass happened in a Google Sheet. It worked, but EF-Map is a web app, so this time the review happens inside it.
There is a page listing every string, with the English next to the current translation and a box to type a correction. It saves as you go. It knows which panel each string appears in and what that panel does, because a key like gateActivity.col.users tells a translator precisely nothing on its own. It flags the strings that are still sitting in English, the ones where a leading space matters, and it warns you live if a correction accidentally drops one of the {curly brace} placeholders that get filled in at runtime, which is the one mistake that would actually break the app rather than just read oddly.
Nothing typed into it touches the live site. The suggestions collect, and get applied deliberately in a later pass. That means the curation can take three weeks without holding anything up.
If German, Ukrainian, Russian, Simplified Chinese or Filipino is your language, or you are simply fluent in one of them, and you use EF-Map and spot something that reads wrong, the instructions and the link are in the localisation thread on the EF-Apps Discord. You do not need to work through all of it. Skimming your language and fixing the handful that stand out is exactly the job.
What I would do differently
Two things.
The first is that I should have caught the misunderstanding in the prompt rather than in the output. The instruction "translate the untranslated" is ambiguous in a way I could not see, because I was carrying context about my own codebase that I never said out loud. The agent was not wrong to read it the way it did.
The second is that this should not have been allowed to build up for six months in the first place. Every feature that shipped between February and now should have had its strings extracted at the time. That is a five minute job per feature and a very long night when you leave it. There is now a script in the repo that can count the untranslated strings on demand, so at least the number is visible from now on.
Roughly half the app still has no translation keys, mostly the help documentation and the tools only I use. The translation files being complete is not the same thing as the app being fully translated, and the ceiling is the wiring, not the translation. That is honest, and it is the next chunk of work.
Related Posts
- One day, three apps, and what an LLM actually costs - the same economics question from a different angle
- Eleven months of voice dictation with AI coding agents - how these prompts get written in the first place, and why they go wrong
- Vibe coding at scale with LLM development - the wider working method behind all of this
- Building the EF-Map visual identity - the design tokens that made theming the new curation page free