GEMINI LABJP
CHAT — From August 26, Google Chat becomes the Ask Gemini hub for searching, drafting, catching up on threads, and managing tasks and events with Workspace context intact. Two days outANDROID — Gemini replaces Google Assistant on Android from September 4, eleven days from now. Now is the time to check any voice shortcuts you built on AssistantROBOTICS — gemini-robotics-er-1.6-preview shuts down on August 31, seven days out. The ER 2 line succeeds it with spatial reasoning, multi-step tool orchestration, and multi-robot coordinationPRICE — Gemini 3.7 Flash introductory pricing is $0.75 input and $3.75 output per million tokens through December 31. From January 1, 2027 it doubles to $1.50 and $7.50FREE — Google AI Studio still offers a free API tier with daily request limits and no credit card. If you only want to see how 3.7 Flash behaves, that is enough to startSCALE — The Gemini app crossed one billion monthly users on August 11. The split is settling in: 3.1 Pro for deep reasoning, the Flash line for production work where speed and unit cost decideCHAT — From August 26, Google Chat becomes the Ask Gemini hub for searching, drafting, catching up on threads, and managing tasks and events with Workspace context intact. Two days outANDROID — Gemini replaces Google Assistant on Android from September 4, eleven days from now. Now is the time to check any voice shortcuts you built on AssistantROBOTICS — gemini-robotics-er-1.6-preview shuts down on August 31, seven days out. The ER 2 line succeeds it with spatial reasoning, multi-step tool orchestration, and multi-robot coordinationPRICE — Gemini 3.7 Flash introductory pricing is $0.75 input and $3.75 output per million tokens through December 31. From January 1, 2027 it doubles to $1.50 and $7.50FREE — Google AI Studio still offers a free API tier with daily request limits and no credit card. If you only want to see how 3.7 Flash behaves, that is enough to startSCALE — The Gemini app crossed one billion monthly users on August 11. The split is settling in: 3.1 Pro for deep reasoning, the Flash line for production work where speed and unit cost decide
Articles/Dev Tools
Dev Tools/2026-08-24Beginner

Gemini quietly drops %1$s from translations, and no reviewer catches it

Format specifiers go missing, turn full-width, or get duplicated when Gemini translates app strings. Here are the four failure modes I keep seeing in production, how to stop them at generation time, and a short check that catches the rest.

Gemini API219localization5indie development16structured output8quality checks

The translation read beautifully. The meaning was intact. But the number in "Saved 12 wallpapers" had simply vanished.

The wallpaper apps I run on my own carry notification and dialog strings in several languages. Once I started using Gemini for that translation work, the thing that cost me the most time was not translation quality at all. It was format specifiers disappearing without a sound.

When a person proofreads a translation, attention goes to meaning. %1$s and %@ carry no meaning, so the eye slides right past them. You find out on a real device, when the spot where a number belongs is blank.

Anything a machine can catch is better left to a machine. This post walks through building that check.

Why placeholders are the one thing review never catches

Format specifiers occupy an odd position in a translation job. They are part of the sentence, and they are the part that must not be translated.

To the model, %1$s is a short run of symbols with no surrounding context. As it rearranges a sentence into natural word order, that symbol gets swept along. The harder the model works on fluency, the more the placeholder looks like something in the way.

Human review does not save you. Checking "does this read correctly" and checking "are the same symbols still present, in the same count" draw on different kinds of attention. While you are doing the first, the second is switched off.

In my case, the more carefully I read, the fewer I caught. Reading for prose pushes symbols into the background.

Four failure modes, and that is all

After keeping notes for a while, the list turned out to be short.

ModeWhat happensHow it shows at runtime
DroppedThe specifier disappears from the translationNo value is substituted; some platforms throw
Full-width%@ becomes %@No substitution happens; the symbols render as text
DuplicatedThe same specifier appears twice or moreArgument count no longer matches; undefined reads
ReorderedSpecifiers move to match target word orderWithout positional syntax, values land in the wrong slots

The fourth one is the awkward case, because reordering is not inherently wrong. If your specifier is positional, like %1$s, the runtime resolves it correctly no matter where it sits in the sentence.

The trouble starts when the source string uses bare %s twice. Swap the order and the values swap too, silently, with no exception raised. A name and a number trade places, and that sentence ships.

One detail worth settling before you automate anything: the two platforms do not spell these the same way. iOS uses %@ for objects and %1$@ when positional, while Android uses %s and %1$s. If you keep both platforms in one translation workflow, decide early whether you normalize to a single internal form and convert on write, or carry both dialects through and let the check handle either. I went with the second, which is why the pattern below accepts both spellings — but the first is a perfectly reasonable choice if your string tables are already in sync.

Which means normalizing your source strings to positional syntax is really step zero. Skip it and the downstream check cannot even decide whether reordering was legitimate.

Stop the breakage at generation time

Before any checking, there are ways to ask that break less often. All of them amount to taking the specifier out of the translation job.

One approach is to swap specifiers for distinctive markers before sending, then restore them afterward. Turning %1$s into something like [[P1]] makes it far more likely the model treats it as an opaque token rather than words to translate.

The other is to receive the result as structured output. Ask for JSON pairing each source string with its translation, and the correspondence is no longer guesswork. Writing the verification step becomes much easier than parsing a long free-form blob. If structured output itself is misbehaving, Fixing Gemini API JSON and structured output failures breaks the symptoms down case by case.

One more note for anyone who has been setting temperature to keep output stable: it is time to plan a different approach, since the sampling parameters are now deprecated. The job of suppressing variation moves from model configuration to your own verification code. Building the check below is part of getting ready for that shift.

A short check to run the moment results come back

The check I actually use does one thing: pull the specifiers out of both strings and compare the counts.

import re
import unicodedata
from collections import Counter
 
PLACEHOLDER = re.compile(r"%(\d+\$)?[@sdfxu]|%%")
 
def placeholders(text: str) -> Counter:
    # Fold full-width symbols to ASCII before counting
    normalized = unicodedata.normalize("NFKC", text)
    return Counter(m.group(0) for m in PLACEHOLDER.finditer(normalized))
 
def has_fullwidth(text: str) -> bool:
    # If normalizing changes the result, full-width characters are present
    raw = Counter(m.group(0) for m in PLACEHOLDER.finditer(text))
    return placeholders(text) != raw
 
def verify(source: str, target: str) -> list[str]:
    src, dst = placeholders(source), placeholders(target)
    problems = []
    if src - dst:
        problems.append(f"dropped: {dict(src - dst)}")
    if dst - src:
        problems.append(f"duplicated: {dict(dst - src)}")
    if has_fullwidth(target):
        problems.append("full-width format specifier present")
    if src and not all("$" in p for p in src):
        # Non-positional specifiers make reordering impossible to validate
        problems.append("source uses non-positional specifiers (reordering unverifiable)")
    return problems

Running this against real cases turned up a few things that are not obvious from reading the code.

First, unicodedata.normalize("NFKC", text) converts %@ into %@. So once you normalize, full-width breakage produces no count difference at all. Detecting it requires a separate move: comparing the result with and without normalization. If you trust the count comparison alone, this failure mode walks straight through.

Second, reordering. Counter is a multiset, so order is invisible to it. That is correct behavior for positional specifiers and useless for non-positional ones. The line that inspects whether the source is positional exists for exactly that reason: the check declares the limits of what it can promise.

Third, %% is an escaped percent sign and never a substitution site. Leaving it out of the pattern makes the regex misread a lone % and produce false positives.

Where to put it, and what to do when it fails

I run this check the instant translations come back, before anything is written to a strings file.

I did consider validating at build time instead, and decided against it. Once a broken translation lands in the resource file, it gets harder to tell which lines are generated output and which are hand-written. Putting the boundary right where external content enters the repository keeps that distinction clean.

Lines that fail get held back for that language only. Rather than redoing the batch, I resend just the offending source string. It usually passes on the second attempt. If it fails three times running, I take that as a signal the source sentence is doing too much, and rewrite it shorter.

I deliberately did not automate the retry loop. The fact that one source string keeps failing is itself the useful information. Automate it away and that signal disappears.

Where to start

Open your source strings and look for bare %s and %@. Converting those to positional form is the single change that makes translated reordering safe, and it costs almost nothing.

If you want to go further and score translation quality rather than just structural integrity, Automating localization QA with the Gemini API covers an evaluation pipeline built on structured output. The check in this post protects against broken strings; that one measures whether the translation is any good.

For how the model choice plays out across locales in practice, A month of optimizing App Store keyword fields with Gemini 2.5 Flash has notes from actual operation.

It is a small check. The relief of catching these before a device does turned out to be much larger than the effort.

Share

Thank You for Reading

Gemini 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 →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Dev Tools2026-08-22
The One Call I Refuse to Hand Gemini During a Phased Release
Day one of a phased release has nowhere near the sample size to tell a healthy build from a broken one. Here is the three-state rollout gate I use, and the narrow job I give Gemini inside it.
Dev Tools2026-08-17
Your Shipped App Still Remembers the Retired Model
Finishing the server-side migration is only half of a model retirement. The older builds of your app still hold the old model name, and once the cutoff passes, rolling back stops being a recovery option. Here is how I moved model resolution onto the server.
Dev Tools2026-07-25
The Day I Stopped Tracking gemini-flash-latest: Batch Design That Survives Silent Model Swaps
A silent model swap pushed my batch rejection rate from 2.1% to 9.8% overnight. The pinning-plus-canary design I moved to, with the harness and numbers.
📚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 →