◉CLAUDE LABJP
●2.1.296 — Sonnet 5.5 cache reads drop to $0.10/Mtok (was $0.20), and the MCP description limit rises to 4,096 characters●2.1.295 — Hooks gain onFailure: "block", so a failed or timed-out hook no longer lets the action through●SONNET 4.5 — Retires on the Claude API on 11/30, 51 days left. Migrate to Sonnet 5.5●Q&A — A long-running report: the console jumps back to the top of history whenever output is appended●HAIKU 5.5 — Code written for Haiku 4.5 hits a 400 error on budget_tokens●NEW — When first requests keep missing, hand over a one-page description of your work first●2.1.296 — Sonnet 5.5 cache reads drop to $0.10/Mtok (was $0.20), and the MCP description limit rises to 4,096 characters●2.1.295 — Hooks gain onFailure: "block", so a failed or timed-out hook no longer lets the action through●SONNET 4.5 — Retires on the Claude API on 11/30, 51 days left. Migrate to Sonnet 5.5●Q&A — A long-running report: the console jumps back to the top of history whenever output is appended●HAIKU 5.5 — Code written for Haiku 4.5 hits a 400 error on budget_tokens●NEW — When first requests keep missing, hand over a one-page description of your work first
Articles/API & SDK
⬡ API & SDK/2026-09-06Intermediate

The translation read perfectly and still crashed at runtime

When you translate app strings with the Claude API, the meaning can be right while the format specifiers quietly break. Here is a severity-aware acceptance check and a repair loop that only re-translates the broken lines.

Claude API126localization4i18nquality gateindie developer21

✦ Premium Article

I was a few days into a 5% staged rollout of a wallpaper app when crash reports started arriving from one language and nowhere else. The only thing I had touched was a batch of in-app strings. No code had changed.

I read the translations top to bottom. Every line was fluent and said the right thing. Nothing looked wrong as prose.

The problem only surfaced when I stopped reading sentences and lined up the symbols instead. The source had %1$@. The translation had %@.

As long as you evaluate a translation as writing, format-specifier damage stays invisible. It isn't a vocabulary problem. It's a syntax problem.

What broke was argument binding, not meaning

A format specifier is a hole where a value gets injected at runtime. Both String(format:) on iOS and String.format on Android assign arguments by counting those holes and checking their types.

A translation model translates sentences. The holes are part of the sentence, so when word order gets rearranged, positional indexes get dropped, or the token gets rewritten to whichever platform convention the model considers idiomatic. There is no malice and no carelessness involved — it falls out of producing a natural sentence.

I started by checking what actually happens when arguments are short or mistyped. Here is real output from Python's % formatting:

"%s / %s" with one argument: TypeError: not enough arguments for format string
"%d" with a string:          TypeError: %d format: a real number is required, not str
"%(n)s" with an empty dict:  KeyError: 'n'
"%s" with two arguments:     succeeded -> 'a'   <- no exception

That last line is the important one. Extra holes crash. Extra arguments do not. Android's String.format has the same asymmetry: referencing an argument that was never supplied raises MissingFormatArgumentException, a type mismatch raises IllegalFormatConversionException, but leaving an argument unused raises nothing at all.

So "a specifier disappeared" and "a specifier appeared" are two different incidents. The first silently loses information. The second takes the device down. Collapsing both into a single "mismatch" verdict means you either fail to stop the dangerous one, or you block releases over the harmless one.

Six ways it breaks

Everything I collected fell into six shapes.

ShapeSourceTranslationEffect
Positional index dropped%1$@ items in %2$d s%@ items in %d sOrder becomes language-dependent
Platform convention swapped%1$@%1$sInvalid on iOS
Specifier missing%1$@ to %2$@Moved %1$@Information disappears
Specifier addedSaved %1$@Saved %1$@ to %2$@Crashes at runtime
Glyph corrupted%1$@%1$@ / % 1$@No longer parsed as a specifier
Escape lost%1$d%%%1$d%A bare % survives into the format string

Glyph corruption shows up more often than I expected whenever a Japanese keyboard sits anywhere in the pipeline. A full-width % is essentially indistinguishable by eye.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦You'll be able to stop format-specifier damage before shipping, even when the translated text reads perfectly
✦You'll be able to treat a dropped specifier and an added one as different incidents, because only one of them crashes
✦You'll be able to add a masked, line-scoped repair loop to a translation pipeline you already run
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $15 for lifetime access
View Membership →

Related Articles

⬡ API & SDK2026-04-30
Building a Production Multilingual Translation SaaS with Claude API — Glossaries, Style, and Domain Adaptation in Practice
A practical, code-first design guide for running a translation SaaS on Claude API: glossaries, style guides, domain adaptation, quality gates, and cost controls that survive real production traffic.
⬡ API & SDK2026-10-08
Before Sonnet 4.5 retires, one script to find the arguments that will return a 400
claude-sonnet-4-5 retires on November 30. Swapping the model ID is not enough: budget_tokens, prefill, and tool_choice will fail. Here is a scanner built from the official migration guide, its real output, and the order I would fix things in.
⬡ API & SDK2026-10-07
Before You Switch Models, Replay Your Own Call Shapes Against the Candidate for a Few Tokens
Change a model ID and the first production request is where a 400 shows up. Here is a small replay script that checks your own call shapes against a candidate model with max_tokens 16 before you switch.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links