CLAUDE LABJP
CLI — Claude Code v2.1.261 landed on September 4. Of its 67 changes, 46 are fixes, and on the CLI side they cluster around input handling and background executionSKILLS — The new /skill-doctor lists which loaded skills go unused and how much context each one costs you every turn. As a starting point for cleanup, it is plainly usefulCOST — In one real-world setup the skill list alone consumed roughly 13,800 tokens per turn. Eighty-four skills had never been called, and duplicates from double-enabled plugins accounted for 41% of the totalLIMIT — New bashOutputMaxChars and taskOutputMaxChars settings raise how much command and background output reaches Claude inline before it is spilled to a file, up to 128K charactersBREAKING — Prompt word-editing keys now follow Bash and keybindingFlavor no longer has any effect. In auto mode, links that embed content into public diagram renderers count as uploadsSAFEGUARDS — Enterprise Frontier Safeguards was announced on September 1. The monitoring data sits in cloud infrastructure the customer controls rather than Anthropic's, and there is no extra chargeCLI — Claude Code v2.1.261 landed on September 4. Of its 67 changes, 46 are fixes, and on the CLI side they cluster around input handling and background executionSKILLS — The new /skill-doctor lists which loaded skills go unused and how much context each one costs you every turn. As a starting point for cleanup, it is plainly usefulCOST — In one real-world setup the skill list alone consumed roughly 13,800 tokens per turn. Eighty-four skills had never been called, and duplicates from double-enabled plugins accounted for 41% of the totalLIMIT — New bashOutputMaxChars and taskOutputMaxChars settings raise how much command and background output reaches Claude inline before it is spilled to a file, up to 128K charactersBREAKING — Prompt word-editing keys now follow Bash and keybindingFlavor no longer has any effect. In auto mode, links that embed content into public diagram renderers count as uploadsSAFEGUARDS — Enterprise Frontier Safeguards was announced on September 1. The monitoring data sits in cloud infrastructure the customer controls rather than Anthropic's, and there is no extra charge
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 API121localization4i18n2quality 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-06-28
Measure Streaming CPU and Dropped Chunks to Stabilize Long Batch Jobs
You start an overnight batch, and by morning only half of it finished. The culprits were CPU pinned during streaming and a quiet connection drop. Here is a monitor wrapper that measures stream CPU and throughput, and resumes from interruptions.
API & SDK2026-06-25
Reach a Remote MCP Server in a Single API Request: Implementing the Messages API MCP Connector
How to call a remote MCP server's tools using only the Messages API's mcp_servers and mcp_toolset—no local MCP client. Covers allowlist/denylist design, response handling, and the pitfalls to avoid before unattended production use.
API & SDK2026-06-19
Grounding Claude on Your Own Knowledge Base with search_result Blocks
How to stop your own RAG setup from losing track of which article it cited, using Claude's search_result content block and structured citations — with real numbers from running it across four sites.
📚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
See all →