How to Build a Translation App with Claude
Building a translation app with Claude comes down to three things: a prompt that constrains output to just the translation, a way to preserve formatting and placeholders across languages, and an API layer that handles streaming, retries, and usage tracking without you writing that infrastructure yourself. Claude is a strong fit for translation because it understands context, tone, and idiom better than traditional machine translation engines — it can handle slang, legal boilerplate, and marketing copy in the same request without separate models.
This guide walks through the actual architecture: how to structure the prompt, how to detect source language, how to preserve HTML/markdown/variables during translation, how to stream long documents, and how to wire it all into an API endpoint your frontend can call directly.
Core architecture
A translation app has four moving parts:
- Input handling — raw text, HTML, markdown, or structured JSON with strings to translate.
- Language detection — either user-selected or auto-detected by Claude itself.
- Translation call — the actual Claude API request with a tightly scoped prompt.
- Output reconstruction — reinserting the translation into the original structure (HTML tags, template variables, JSON keys).
Most translation apps skip step 4 and immediately run into bugs: lost {{username}} placeholders, broken HTML tags, or markdown links that stop pointing to the right URL. Design for this from day one.
Writing the translation prompt
Keep the system prompt strict. Claude will happily add commentary ("Here's the translation:") unless you explicitly forbid it.
You are a translation engine. Translate the user's text from {source_lang}
to {target_lang}. Preserve all HTML tags, markdown syntax, and placeholder
variables like {{name}} exactly as they appear. Do not add explanations,
notes, or quotation marks around the output. Return only the translated text.
A real request using the Messages API looks like this:
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"content-type": "application/json",
"x-api-key": process.env.ANTHROPIC_API_KEY,
"anthropic-version": "2023-06-01"
},
body: JSON.stringify({
model: "claude-sonnet-4-5",
max_tokens: 1024,
system: "You are a translation engine. Translate from Spanish to German. Preserve HTML tags and {{placeholders}} exactly. Return only the translated text.",
messages: [
{ role: "user", content: "<p>Hola {{name}}, bienvenido de nuevo.</p>" }
]
})
});
If you're routing this through SubToAPI instead of managing Anthropic keys and infrastructure directly, the request shape is nearly identical — you just point at your own API key:
curl https://api.subtoapi.app/v1/messages \
-H "Authorization: Bearer $SUBTOAPI_KEY" \
-H "content-type: application/json" \
-d '{
"model": "claude-sonnet-4-5",
"max_tokens": 1024,
"system": "Translate from Spanish to German. Preserve HTML and {{placeholders}}. Return only the translated text.",
"messages": [{"role": "user", "content": "<p>Hola {{name}}, bienvenido de nuevo.</p>"}]
}'
The benefit here is that sub_live_ keys are per-application, usage is logged automatically, and you don't have to build your own metering before shipping. See /docs/messages for the full request reference.
Auto-detecting the source language
Instead of forcing users to pick a source language, let Claude detect it in the same call:
Detect the language of the input text, then translate it to {target_lang}.
Return only the translated text — no language name, no explanation.
This removes a UI step and handles mixed-language input gracefully (common in support tickets or user-generated content). If you need the detected language for logging or analytics, ask for it as structured output instead:
Respond with JSON only: {"detected_language": "...", "translation": "..."}
Parse that JSON on your backend rather than regex-matching free text — Claude is reliable at following a strict JSON schema when you specify it exactly in the prompt.
Preserving structure in longer documents
For HTML emails, markdown docs, or JSON-based i18n files, don't translate the whole blob as a single string if it contains nested structure you need intact. Two reliable patterns:
- Chunk by translatable unit. Extract just the text nodes (not tags/attributes) into an array, translate the array as one request with a numbered list, and reinsert by index.
- Translate the whole document with explicit preservation rules. Works fine for shorter HTML snippets and markdown where the structure is simple enough that Claude won't drop or mangle tags.
For i18n JSON files ({"welcome_message": "Hello!"}), translate the values only and keep the keys untouched — this is the simplest case and Claude handles it well when you tell it explicitly not to translate keys.
Streaming for long translations
Document-length translations (contracts, articles, release notes) can take several seconds. Stream the response so users see output appear progressively instead of staring at a spinner:
const res = await fetch("https://api.subtoapi.app/v1/messages", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SUBTOAPI_KEY}`,
"content-type": "application/json"
},
body: JSON.stringify({
model: "claude-sonnet-4-5",
max_tokens: 4096,
stream: true,
system: "Translate from English to Japanese. Return only the translation.",
messages: [{ role: "user", content: longDocumentText }]
})
});
const reader = res.body.getReader();
// read and parse SSE chunks, append text deltas to the UI as they arrive
Full streaming setup — including SSE event parsing — is covered in /docs/streaming.
Batching multiple languages
If you need one source text translated into five target languages at once, don't fire five sequential requests from the client. Either run them in parallel from your backend, or if volume is high, queue them and process with concurrency limits so you don't hit rate limits. SubToAPI's usage metadata on every response (token counts, model used) makes it easy to track cost per language pair if you're billing translation as a feature inside your own product.
Handling failures gracefully
Translation requests can fail for the usual reasons — network errors, rate limits, oversized input. Build retry logic with exponential backoff for transient failures, and set a sane max_tokens so very long documents don't get silently truncated mid-sentence. If truncation happens, detect it by checking the stop_reason field in the response and re-request the remainder rather than showing a half-finished translation to the user.
Getting started
For a minimal working app: pick a model, write a strict system prompt, decide your input format (plain text, HTML, or JSON), and wire up one API endpoint. Check /docs/quickstart for a working request-and-response example, and /pricing if you want predictable per-seat costs instead of raw token billing while you're building. A free trial at /signup is enough to prototype the whole flow before committing to a plan.
FAQ
Does Claude outperform traditional machine translation for app use cases? For context-heavy or idiomatic text — support tickets, marketing copy, legal disclaimers — Claude generally produces more natural output than rule-based MT engines because it understands tone and context rather than translating word-by-word.
How do I stop Claude from adding extra commentary to translations? Be explicit in the system prompt: "Return only the translated text, no explanations, no quotation marks." Claude follows strict formatting instructions reliably when they're stated directly rather than implied.
Can I translate structured content like HTML or JSON without breaking it? Yes — either instruct Claude to preserve tags and placeholders exactly, or extract just the translatable text nodes, translate them as a batch, and reinsert them into the original structure programmatically for guaranteed integrity.