I Translated 75 Blog Posts to English in 60 Minutes with Claude Code — $0 in API Costs

·AI for Work·7 min read

Translated from the original Korean post. 한국어 원문 보기 →

It started with a zero

Sunday afternoon. I was checking my blog's Google index status and saw a number: 0. Seventy-five posts, and not one page in Google's index. Indexing is a solvable problem — register in Search Console and move on — but while I was in there, another thought attached itself. If I'm touching search visibility anyway, why not open up English search too?

Browser translation exists, so a Korean post is readable enough. Readability isn't the problem. Discovery is. Google indexes a page in whatever language the page is written in. When an English speaker searches "docker to podman migration," a Korean post isn't even a candidate. An English page has to actually exist to show up in English search.

So I decided to move all 75 posts into English. Heavy call for a Sunday afternoon, but the estimates changed my mind.

Three options, three price tags

There were three ways to do this.

Hire a professional translator. Technical translation runs ₩30,000–50,000 per page. At an average of 6,000 characters per post across 75 posts, that's roughly ₩3,000,000. Best quality by far, and not an amount I'm spending on a personal blog.

The Anthropic API. With Opus, figure ~8k input tokens and ~5k output tokens per post — call it ₩500–700 each. Around ₩40,000–70,000 for all 75. Not bad. What bugged me was that I'm already paying for something every month.

The Claude Max subscription. I use Claude Code on a subscription. And Claude Code has a -p flag — non-interactive mode, callable from a script instead of a chat window. It runs inside the subscription limits, so the marginal cost is zero.

Same model, same quality, different bill. The answer picked itself.

Using Claude Code like an API

-p does one thing. Give it a prompt, it prints a response and exits.

echo "1+1은?" | claude -p --output-format json | jq -r .result

Add --output-format json and the response comes back structured. Parse it, drop it into a pipeline, and it's an API substitute. --append-system-prompt lets you pin a system prompt in place. That's where all the translation rules went: don't touch code blocks or image URLs, preserve sentence rhythm, don't smooth everything into marketing tone.

From a Node script, I spawn it. Long post bodies blow past the argv length limit if you pass them as arguments, so they go in over stdin.

const child = spawn("claude", [
  "-p", "--model", "opus",
  "--output-format", "json",
  "--append-system-prompt", SYSTEM,
], { stdio: ["pipe", "pipe", "pipe"] });

child.stdin.write(prompt);
child.stdin.end();

All of that you can get from the docs. The trouble came from somewhere the docs don't cover.

First hole I fell into: a 401 out of nowhere

First run, 401 auth error. My subscription login was fine, but it told me my credentials were bad.

I stared at it for a while before finding it. Environment variables. If ANTHROPIC_API_KEY is set in the shell, Claude Code prefers that key over subscription auth. My environment had a key from another project sitting in it, plus a few related variables, and the spawned child process inherited every one of them. A process trying to authenticate by subscription was knocking on the API door holding the wrong key.

Fix was to strip those variables at spawn time.

const env = {};
for (const [k, v] of Object.entries(process.env)) {
  if (k.startsWith("ANTHROPIC_")) continue;
  env[k] = v;
}
const child = spawn("claude", args, { env });

Environment inheritance is decades-old Unix behavior, and I still trip on it every single time. Child processes inherit way too much of their parents' baggage.

The pipeline: metadata separate, body separate

Each post takes two calls. One for metadata — title, description, tags — as JSON. One for the markdown body. Bundling them into a single call would be tidier, but expecting a model to nail exact JSON structure at the tail end of a long body translation is a low-probability bet. Splitting them separates the failure points.

I built in re-run handling too. Each translated file carries the source's date as sourceDate in its frontmatter, and a re-run skips anything whose source hasn't changed. If the batch dies halfway, I just run it again. If I edit an original later, only that post gets retranslated.

원문 75편 (content/posts/)
  → 메타 번역 (JSON)
  → 본문 번역 (마크다운)
  → content/posts-en/에 저장, 같은 slug 유지
  → /en/post/[slug] 페이지 + hreflang으로 원문과 연결

Reusing the slug matters. When the Korean original and the English translation pair up under the same address scheme, a single hreflang declaration is enough for Google to serve whichever side matches the searcher's language.

The numbers

항목 결과
번역한 글 75편
소요 시간 60분
실패 0편
편당 처리 26초 ~ 104초
추가 비용 0원
API로 했다면 약 4~7만 원
외주였다면 약 300만 원

I kicked it off, went to work on something else, then came back to the log. The line that looked wrong was the zero failures. A batch job like this is supposed to have two or three corpses in it for reasons nobody can explain. Splitting the calls and piping over stdin probably did most of the work. Remove failure points ahead of time and you get fewer failures. Obvious. Also the thing I forget every time.

I checked quality by spot-check. Opened a few translations against the originals and the sentence rhythm was still there. Short sentences stayed short, self-deprecation stayed self-deprecating. I think pinning "don't polish this into marketing copy" into the system prompt is what did it. It didn't read like the smooth, flat output a translator produces — it read like something a person wrote.

The limits, stated plainly

I've been calling this free, but what actually happened is that it fit inside a subscription I already pay for. That's a chunk of my Max limit spent. If I'd been running other work in parallel during that hour, I'd have felt the ceiling. At a few hundred posts, you're not finishing in one day — you'd split it across runs.

And this is an interactive tool wedged into a batch pipeline, so it's the wrong shape for anything that runs continuously, like a service backend. For that, just use the API. Tools have their places.

I also didn't review all 75 translations line by line. I spot-checked and shipped, and there's an awkward sentence sitting somewhere in there. I'd rather fix them as they surface. If I'd waited for a complete review, neither this post nor the translations would exist yet.

What 60 minutes changed

Started Sunday afternoon, done before dinner. The blog now has 75 Korean posts and 75 English ones, and Google can finally serve a different page per language. Whether any English-language traffic actually shows up is a question for a few months from now. Indexing has always been slow like that.

One thing is settled, though. For a blog one person runs alone, "a full English edition" used to not be an option. You had to burn a serious amount of either money or time. Now it's a Sunday afternoon. This keeps happening lately — not impossible things becoming possible, but expensive things getting cheap.

One worry left. What happens when an English-speaking reader hits one of these and says it reads like a translation. Well. I'll fix it then.

Was this post helpful?

One click helps me write the next one

#Claude Code#AI Automation#Translation#CLI#Blogging