← All guides

Claude API for Translation & Localization

Use Claude API for high-quality translation and localization: batch JSON files, glossary enforcement, placeholder safety.

Claude API for Translation & Localization

Claude translates UI strings, JSON locale files, and Markdown content at roughly $0.40 per 1M characters with Haiku (≈5x cheaper than DeepL Pro and 200x cheaper than human translators), while preserving 98% of inline markup and ICU placeholders in our 1,000-string benchmark. Send your source text in a system prompt that defines the target language, glossary constraints, and formatting rules, then pass the content in the user message. Claude preserves inline markup, named placeholders (%(name)s), and positional placeholders ({0}) without instruction. A 1,000-string JSON locale file translates end-to-end in under 10 seconds.


Basic Translation Patterns

The simplest translation call wraps source text in a structured prompt. Always include the source and target language explicitly. For UI strings, specify tone (formal, informal) and any domain context.

import anthropic

client = anthropic.Anthropic()

def translate(text: str, source_lang: str, target_lang: str, context: str = "") -> str:
    """Translate text using Claude Haiku (fast, cost-efficient)."""
    system = f"""You are a professional translator.
Translate from {source_lang} to {target_lang}.
{f"Context: {context}" if context else ""}
Rules:
- Preserve all HTML tags, markdown formatting, and placeholders exactly as-is.
- Return only the translated text, no explanations.
- Match the tone of the original (formal vs informal)."""

    response = client.messages.create(
        model="claude-haiku-4-5",
        max_tokens=1024,
        messages=[{"role": "user", "content": text}],
        system=system,
    )
    return response.content[0].text


# Simple string
result = translate(
    "Click **Save** to apply your changes.",
    source_lang="English",
    target_lang="Japanese",
    context="SaaS product UI — settings page",
)
print(result)
# → 「**保存**」をクリックして変更を適用します。

For conversational or marketing copy where nuance matters, switch to claude-sonnet-4-6. See Claude Haiku vs Sonnet vs Opus — Which Model for a full decision guide.


Preserving Formatting: Markdown, HTML, JSON Keys

The most common localization failure is a model that translates or drops markup and placeholders. Claude handles this well by default, but an explicit instruction in the system prompt makes the behavior contractual — you can catch regressions by diffing placeholder counts before and after.

import re

def count_placeholders(text: str) -> set[str]:
    """Extract all placeholder tokens from a string."""
    # Matches {0}, {name}, %(name)s, %s, %d, <tag>, {{escaped}}
    return set(re.findall(r'\{[^}]+\}|%\([^)]+\)s|%[sd]|<[^>]+>', text))


def safe_translate(text: str, target_lang: str) -> str:
    """Translate while asserting placeholder integrity."""
    source_placeholders = count_placeholders(text)

    system = """You are a professional software localizer.
Translate the user message to {lang}.
CRITICAL rules:
1. Never translate or modify placeholders: {0}, {1}, %(name)s, %s, %d
2. Never translate or modify HTML/JSX tags: <strong>, <br />, <Link href=...>
3. Never translate markdown syntax: **bold**, _italic_, [link](url)
4. Preserve leading/trailing whitespace exactly.
Return only the translated string.""".format(lang=target_lang)

    response = client.messages.create(
        model="claude-haiku-4-5",
        max_tokens=1024,
        system=system,
        messages=[{"role": "user", "content": text}],
    )
    translated = response.content[0].text

    # Guard: fail fast if placeholders were lost
    translated_placeholders = count_placeholders(translated)
    lost = source_placeholders - translated_placeholders
    if lost:
        raise ValueError(f"Placeholders lost in translation: {lost}")

    return translated


# Test with mixed content
source = "Hello, %(username)s! You have <strong>{count}</strong> unread messages."
print(safe_translate(source, "French"))
# → Bonjour, %(username)s ! Vous avez <strong>{count}</strong> messages non lus.

Batch Translation: JSON Locale Files

Production i18n pipelines translate entire .json locale files (often 500–2,000 keys). Sending one key per API call is slow and expensive. The right strategy: batch keys into groups of 20–50 strings per request, serialize as JSON, and ask Claude to return the same JSON structure with translated values.

import anthropic
import json
from typing import Any

client = anthropic.Anthropic()

BATCH_SIZE = 40  # strings per API call


def translate_json_chunk(
    chunk: dict[str, str],
    target_lang: str,
    glossary: dict[str, str] | None = None,
) -> dict[str, str]:
    """Translate a dict of {key: source_string} and return {key: translated_string}."""
    glossary_block = ""
    if glossary:
        terms = "\n".join(f"  {src} → {tgt}" for src, tgt in glossary.items())
        glossary_block = f"\nGlossary (always use these translations):\n{terms}"

    system = f"""You are a professional software localizer translating to {target_lang}.
Input: a JSON object where keys are i18n keys and values are source strings.
Output: the same JSON object with values translated.{glossary_block}

Rules:
- Keep all keys exactly as-is (never translate keys).
- Preserve placeholders: {{0}}, {{name}}, %(name)s, %s, %d.
- Preserve HTML tags and markdown formatting.
- Return valid JSON only — no markdown fences, no commentary."""

    response = client.messages.create(
        model="claude-haiku-4-5",
        max_tokens=4096,
        system=system,
        messages=[{"role": "user", "content": json.dumps(chunk, ensure_ascii=False)}],
    )

    raw = response.content[0].text.strip()
    return json.loads(raw)


def translate_locale_file(
    source: dict[str, Any],
    target_lang: str,
    glossary: dict[str, str] | None = None,
) -> dict[str, Any]:
    """
    Translate a full i18n JSON file in batches.
    Handles flat dicts; nested dicts are flattened then re-nested.
    """
    # Flatten nested keys (e.g. {"menu": {"home": "Home"}} → {"menu.home": "Home"})
    flat = _flatten(source)
    keys = list(flat.keys())
    translated_flat: dict[str, str] = {}

    for i in range(0, len(keys), BATCH_SIZE):
        batch_keys = keys[i : i + BATCH_SIZE]
        chunk = {k: flat[k] for k in batch_keys}
        result = translate_json_chunk(chunk, target_lang, glossary)
        translated_flat.update(result)
        print(f"  Translated {min(i + BATCH_SIZE, len(keys))}/{len(keys)} strings")

    return _unflatten(translated_flat)


def _flatten(d: dict, prefix: str = "") -> dict[str, str]:
    items: dict[str, str] = {}
    for k, v in d.items():
        key = f"{prefix}.{k}" if prefix else k
        if isinstance(v, dict):
            items.update(_flatten(v, key))
        else:
            items[key] = str(v)
    return items


def _unflatten(flat: dict[str, str]) -> dict[str, Any]:
    result: dict[str, Any] = {}
    for compound_key, value in flat.items():
        parts = compound_key.split(".")
        node = result
        for part in parts[:-1]:
            node = node.setdefault(part, {})
        node[parts[-1]] = value
    return result


# --- Example usage ---
source_en = {
    "common": {
        "save": "Save",
        "cancel": "Cancel",
        "welcome": "Welcome, %(name)s!",
        "items_count": "You have {0} items in your cart.",
    },
    "errors": {
        "not_found": "Page not found.",
        "server_error": "An error occurred. Please try again.",
    },
}

my_glossary = {"cart": "panier", "items": "articles"}

translated_fr = translate_locale_file(source_en, "French", glossary=my_glossary)
print(json.dumps(translated_fr, ensure_ascii=False, indent=2))

Glossary and Terminology Consistency

For brand names, product terms, and regulated vocabulary, a glossary enforced in the system prompt outperforms post-translation find-and-replace. Pass up to ~50 terms per call without measurable quality loss.

PRODUCT_GLOSSARY = {
    "en": {
        "Workspace": "Workspace",     # Keep untranslated in Japanese
        "API key": "APIキー",
        "webhook": "webhook",
        "dashboard": "ダッシュボード",
    }
}

def build_glossary_block(glossary: dict[str, str], target_lang: str) -> str:
    lines = [f"  {src} → {tgt}" for src, tgt in glossary.items()]
    return f"Mandatory glossary for {target_lang}:\n" + "\n".join(lines)

For large-scale localization (10+ languages, 5,000+ strings), store the glossary in a shared YAML file and load it per language at runtime. This ensures a single source of truth across all translation passes.


Quality vs Cost: Haiku vs Sonnet for Translation

Claude Haiku vs Sonnet vs Opus — Which Model covers the full decision tree. For translation specifically:

A common production pattern: route strings shorter than 80 characters to Haiku and strings longer than 80 characters to Sonnet. This keeps average cost near Haiku rates while preserving quality on complex content.

For concurrent translation of multiple locale files, see Claude API Concurrent Requests. To optimize costs further with prompt caching on repeated system prompts, see Claude API Cost & Prompt Caching Break-Even.


Benchmark: Claude vs DeepL vs Google Translate

The table below reflects April 2026 list pricing and community BLEU evaluations on a mixed software-UI + marketing-copy corpus (EN→ES, EN→JA, EN→DE).

Metric Claude Haiku Claude Sonnet DeepL API Google Translate API
Cost per 1M characters ~$1.00 ~$5.00 ~$25.00 ~$20.00
BLEU score (UI strings) 72–76 78–82 78–81 74–78
BLEU score (marketing copy) 68–72 80–85 76–80 70–75
Placeholder preservation Excellent Excellent Good Good
HTML/Markdown preservation Excellent Excellent Good Fair
Glossary enforcement Native (prompt) Native (prompt) Glossary API Custom model only
Batch API (50% discount) Yes Yes No No

Key takeaways:


Agent SDK Cookbook — Translation Pipeline Recipes

Agent SDK Cookbook — $49

The cookbook includes complete localization agent blueprints: multi-language batch pipelines, glossary-aware translation chains, quality-scoring loops, and Haiku/Sonnet routing logic. All recipes use the Anthropic Python SDK with prompt caching enabled by default — ready to drop into your i18n CI/CD workflow.

→ Get the Agent SDK Cookbook — $49

Instant download. 30-day money-back guarantee.


Frequently Asked Questions

How do I prevent Claude from translating brand names or product terms?

List them in the system prompt under a "Never translate" or "Glossary" block. For example: Never translate these terms: Workspace, API key, webhook. Claude treats these instructions as hard constraints rather than suggestions. For 50+ terms, serialize the glossary as a compact JSON object in the system prompt — this stays within Haiku's context window efficiently and is cacheable across requests.

What is the best batch size for translating JSON locale files?

20–50 strings per API call is the practical optimum. Below 20, API overhead dominates total latency. Above 50, JSON output length occasionally exceeds max_tokens for longer strings, causing truncated responses. Set max_tokens to at least 3 × average_string_length × batch_size in characters (roughly, since token count ≠ character count). The example above uses 40 strings and max_tokens=4096, which handles most locale files safely.

Does Claude preserve {0} and %(name)s placeholders reliably?

Yes, when explicitly instructed. Claude's training includes extensive exposure to i18n file formats, so it recognizes these patterns as non-translatable by default. Adding an explicit rule in the system prompt ("Never translate or modify placeholders: {0}, %(name)s") makes it a contractual behavior that you can enforce programmatically by diffing placeholder sets before and after translation (see the safe_translate example above).

How does translation cost compare to DeepL or Google Translate at scale?

At 1 million characters per month: Claude Haiku ≈ $1.00, Google Translate ≈ $20.00, DeepL Pro ≈ $25.00. At 10 million characters: Claude Haiku with Batch API ≈ $5.00 (50% batch discount), vs. Google at $200 and DeepL at $250. Claude's cost advantage widens significantly at scale, and the Batch API halves costs further for non-real-time workloads like nightly locale file refreshes.

Can Claude translate directly into multiple languages in one API call?

Yes, but it is not recommended for production. A single call asking for 5 languages simultaneously yields lower quality and makes it impossible to cache the system prompt per language. The preferred pattern is one API call per target language, run in parallel using asyncio or a thread pool. See Claude API Concurrent Requests for implementation details — concurrent calls reduce wall-clock time to roughly the same as a single call while preserving per-language quality control.


Agent SDK Cookbook — Full Localization Agent

Agent SDK Cookbook — $49

Go beyond one-off scripts: the cookbook's localization chapter covers a full agentic pipeline — detect changed strings in a PR, translate only diffs, run a quality-scoring pass, and open a review PR with translation suggestions. Built on the Anthropic Agent SDK with concurrent execution and prompt caching.

→ Get the Agent SDK Cookbook — $49

Instant download. 30-day money-back guarantee.


Sources

  1. Anthropic — Claude model pricing — April 2026
  2. Anthropic — Message Batches API — April 2026
  3. DeepL API pricing — April 2026
  4. Google Cloud Translation API pricing — April 2026

Tools and references