●PRO35 — Gemini 3.5 Pro is reported to target July 17, though the date, context window, and pricing all remain unconfirmed by Google●REBUILD — The model is said to be a ground-up rebuild after the 2.5 Pro architecture was scrapped over structural failures in recursive tool-calling and SVG generation●FLASH35 — Gemini 3.5 Flash is the newest broadly available release, tuned for faster, lower-cost coding, agents, multimodal, and long-document work●SAATHI — ATL Saathi, built on Gemini 3.5 Flash, begins piloting in 100 Indian schools as a 24/7 planning and training assistant for Tinkering Lab educators●SEA — In Southeast Asia, nearly 70% of prompts arrive in native languages — Vietnam 89%, Thailand 87%, Indonesia 84% — and 3 in 4 requests come from mobile●SHEETS — From July 15, AI Expanded Access licenses raise the usage limits for Fill with Gemini and the AI function in Sheets●PRO35 — Gemini 3.5 Pro is reported to target July 17, though the date, context window, and pricing all remain unconfirmed by Google●REBUILD — The model is said to be a ground-up rebuild after the 2.5 Pro architecture was scrapped over structural failures in recursive tool-calling and SVG generation●FLASH35 — Gemini 3.5 Flash is the newest broadly available release, tuned for faster, lower-cost coding, agents, multimodal, and long-document work●SAATHI — ATL Saathi, built on Gemini 3.5 Flash, begins piloting in 100 Indian schools as a 24/7 planning and training assistant for Tinkering Lab educators●SEA — In Southeast Asia, nearly 70% of prompts arrive in native languages — Vietnam 89%, Thailand 87%, Indonesia 84% — and 3 in 4 requests come from mobile●SHEETS — From July 15, AI Expanded Access licenses raise the usage limits for Fill with Gemini and the AI function in Sheets
What language should your system instruction be in? Measuring three approaches when most prompts arrive in the user's language
Keep the system instruction in English, or translate it into the user's language? I measured input tokens per language with countTokens, then lined up output-language match and schema compliance to find where nine tokens is enough.
A screen where someone had asked in Thai was answering in English.
I was checking the description-generation flow of my wallpaper app on a Thai test device. Japanese and English were fine. Thai came back in English roughly nine times out of a hundred. Nothing crashed. No errors surfaced. The answer simply arrived in a language the user could not read.
My system instruction at the time was a single English text shared across every language. Only the user's input language changed; the instruction stayed put. That had been good enough for a long time, mostly because Japanese and English were the only languages I had ever looked at seriously.
Google's Southeast Asia report puts roughly 70% of Gemini app prompts in the region in a local language — 89% for Vietnamese, 87% for Thai, 84% for Indonesian. My own app's mix may not match those figures. But "the prompt will probably arrive in English" is clearly becoming the exceptional assumption, not the safe one.
So should the instruction be translated too? Does it help, and what does it cost? Rather than deciding by feel, I measured.
Three approaches, on the same footing
None of these is exotic. What I lacked was a record of measuring them side by side.
Approach
What goes in system_instruction
Effect on input tokens
Intended benefit
A: English only
One English text for every language
Baseline
One place to maintain
B: English + language pin
The English text plus one line naming the output language as a BCP-47 tag
Marginal increase
Fixes the output language only
C: Mirror the user's language
The instruction itself translated into the user's language
Large, and highly language-dependent
Aligns instruction and input
C looks like the principled choice. Instruction and input share a language, so the model has less to reconcile. Before measuring, I assumed it would win.
Deciding what counts as success, before measuring
The most dangerous way to compare these is to read the outputs and pick whichever "feels better." Once three or four languages are in play, your impression of the one language you actually read drives the whole conclusion.
So I fixed three metrics first.
Output-language match — how often the response body's language matched the intended one
Schema compliance — how often the enum fields defined in response_schema came back with defined values
Input tokens — tokens per request as measured by countTokens
Separating 1 and 2 mattered more than I expected. The language can be right while the structure quietly breaks, and the reverse happens too.
✦
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
✦Real measurements showing the same system instruction ranging from 214 to 612 tokens across languages, plus the countTokens procedure to measure your own
✦A side-by-side comparison of English-only, English-plus-a-language-pin, and full mirroring across 500 requests per language, on both output-language match and schema compliance
✦The metric that got worse when I translated the instruction, and how to tell when nine extra tokens gets you the same result
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.
Input tokens spread by more than 2x across languages
I started with the cost side. Here is the same twelve-sentence system instruction, translated into five languages and run through countTokens.
# measure_instruction_tokens.py# Measure the same instruction across languages and report inflation vs. Englishfrom google import genaiclient = genai.Client() # reads GEMINI_API_KEY from the environmentMODEL = "gemini-flash-latest"INSTRUCTIONS = { "en": open("instructions/en.txt", encoding="utf-8").read(), "ja": open("instructions/ja.txt", encoding="utf-8").read(), "id": open("instructions/id.txt", encoding="utf-8").read(), "vi": open("instructions/vi.txt", encoding="utf-8").read(), "th": open("instructions/th.txt", encoding="utf-8").read(),}def count(text: str) -> int: # Measured as contents, not as system_instruction — see the note below return client.models.count_tokens(model=MODEL, contents=text).total_tokensbase = count(INSTRUCTIONS["en"])for lang, text in INSTRUCTIONS.items(): n = count(text) print(f"{lang}\t{n:>4} tokens\t{n / base:.2f}x\t{len(text):>4} chars")
Results for my instruction:
Language
Input tokens
vs. English
Difference
English (en)
214
1.00x
—
Indonesian (id)
268
1.25x
+54
Japanese (ja)
341
1.59x
+127
Vietnamese (vi)
498
2.33x
+284
Thai (th)
612
2.86x
+398
Mirroring into Thai spends about 2.9x the tokens of English to say the same thing — 398 extra tokens per request. My description pipeline runs up to about 1,200 requests a day. Even assuming Thai users are only 20% of the mix, that is over 90,000 tokens a day attributable purely to translating the instruction.
The spread mattered more than any single number. Indonesian at 1.25x is not worth thinking about. Thai at 2.86x is. What I had been treating as one thing — "multilingual support" — turned out to be several different things once cost entered the picture.
One snag worth flagging: passing an instruction through GenerateContentConfig means it will not show up in a plain contents token count. When you want a figure close to actual billing, reconcile against response.usage_metadata.prompt_token_count after generation. The gap between estimates and billing is the same one I worked through in building a budget gate on countTokens.
Output-language match — where nine tokens got me
Now the benefit side. I ran 4 languages x 3 approaches x 500 requests on gemini-flash-latest and classified the language of each response body. Classification uses Unicode script composition rather than asking a model, because otherwise the instrument and the subject share the same failure mode.
Approach B adds exactly one line to the end of the English instruction:
Respond only in the language identified by this BCP-47 tag: th-TH
countTokens puts that at 9 tokens.
Language
A: English only
B: English + pin (+9 tok)
C: Mirror
Japanese
96.4%
99.6%
99.7%
Indonesian
94.1%
99.3%
99.4%
Vietnamese
92.7%
99.0%
99.2%
Thai
91.2%
99.1%
99.4%
Thai went from 91.2% to 99.1%. For nine tokens.
Mirroring reached 99.4% — 0.3 points ahead of the pin. That is one or two responses out of 500. For those one or two, Thai requests were paying 398 extra tokens each.
My prediction was wrong. What helped was not aligning the instruction's language with the input, but naming the output language explicitly as a single constraint. In hindsight it reads as obvious. The model could read the English instruction perfectly well; it simply had never been told which language to answer in.
That remaining 0.9% does not disappear here, of course. Driving match rate toward 100% is separate work, and I kept the design for measuring drift as a continuous quantity in measuring contamination rate and tightening it in stages. The question here stops at which approach to build on.
What mirroring quietly cost me
This is the part that made the measurement worth doing.
On the second metric — schema compliance — only C dropped.
Language
A: English only
B: English + pin
C: Mirror
Japanese
99.4%
99.2%
98.9%
Indonesian
99.5%
99.4%
98.1%
Vietnamese
99.3%
99.2%
97.3%
Thai
99.4%
99.1%
96.8%
Lining up the failures made the cause obvious. When the instruction was translated, the translation had also translated the field names and enum values.
The English instruction said:
Set "mood" to one of: calm, vivid, minimal.
In the Thai version, calm, vivid, and minimal had each become a Thai adjective. As prose for a human reader, that is a correct translation. But the response_schema enum is still in English, so the model follows the translated wording and returns values the enum does not contain. The better the translation, the more reliably this breaks.
So C's 96.8% was not a limit of mirroring. It was a hole in my translation step. Carving out what must never be translated closes it.
# Separate what may be translated from what must survive verbatimPROTECTED = ("mood", "calm", "vivid", "minimal", "aspect_ratio", "safe_zone")def assert_identifiers_intact(translated: str) -> None: """Fail the translation step if any protected identifier was lost.""" missing = [w for w in PROTECTED if w not in translated] if missing: raise ValueError(f"Translation dropped identifiers: {missing}")
With that check in place, Thai mirroring came back to 99.0% schema compliance — essentially level with B.
Which, good as that is, leaves C in an awkward position. You maintain a translation step and a protected-token check, retranslate five languages every time the instruction changes, and pay up to 398 extra tokens per request — to gain 0.3 points of output-language match. For my workload, that did not balance.
Before / After — collapsing it to one call site
With the answer in hand, I moved the implementation. Before, the per-language branch lived at the call site.
# Before — the caller has to know about languagesdef generate_description(text: str, lang: str) -> str: if lang == "ja": si = JA_INSTRUCTION # translated instruction elif lang == "th": si = TH_INSTRUCTION # translated instruction else: si = EN_INSTRUCTION resp = client.models.generate_content( model=MODEL, contents=text, config=genai.types.GenerateContentConfig( system_instruction=si, response_mime_type="application/json", response_schema=DESCRIPTION_SCHEMA, ), ) return resp.text
Every new language grows the elif chain, and every instruction edit means touching every translation. I once ran a stale Japanese instruction for two weeks without noticing, precisely because of this shape.
After, the instruction is one English text and the only thing that varies is a BCP-47 tag.
# After — one English instruction; only the final line changesfrom google import genaifrom google.genai import typesBASE_INSTRUCTION = open("instructions/en.txt", encoding="utf-8").read()# UI language to BCP-47. Unknown languages fall back to English rather than guessing.LOCALE_TAGS = { "ja": "ja-JP", "th": "th-TH", "vi": "vi-VN", "id": "id-ID", "en": "en-US",}def build_system_instruction(lang: str) -> str: tag = LOCALE_TAGS.get(lang, "en-US") return ( f"{BASE_INSTRUCTION}\n" f"Respond only in the language identified by this BCP-47 tag: {tag}" )def generate_description(text: str, lang: str) -> str: resp = client.models.generate_content( model=MODEL, contents=text, config=types.GenerateContentConfig( system_instruction=build_system_instruction(lang), response_mime_type="application/json", response_schema=DESCRIPTION_SCHEMA, ), ) # Confirm real cost from usage_metadata rather than estimating log_usage(lang, resp.usage_metadata.prompt_token_count, resp.usage_metadata.candidates_token_count) return resp.text
Five instruction files became one, and the per-language maintenance cost went away. A side effect: the system instruction is now byte-identical across every language, which is exactly the shape context caching wants.
Falling back to English for anything outside LOCALE_TAGS is deliberate. Assembling an arbitrary tag from the device locale and passing it straight through led the model to attempt generation in low-resource languages, with a visible quality drop. Holding only the languages you have decided to support feels safer to me.
How I would choose
Rearranged into the order I would actually decide in:
Your situation
What I'd pick
Why
Output language is simply unstable
B: English + pin
Nine tokens moves match from the low 90s to 99%+. Start here
Structured output (enums, JSON) is the point
B
Mirroring only adds a path for translation to break identifiers
The instruction itself encodes cultural norms (honorifics, registers)
C: Mirror, with protected-token checks
If English cannot express the distinction, the extra tokens earn their place
Long instruction, shared across languages
B + context caching
An identical instruction is what caching requires
Thai or Vietnamese are a large share of traffic
B, emphatically
At 2.3–2.9x inflation, mirroring lands hardest exactly here
Four of those five rows point to B for me. That is a statement about my workload, which is mostly structured output. For an app where conversational warmth is the product, 398 tokens is a bargain.
What decided it was not which approach is better in the abstract, but which failure hurts. When an enum breaks, my wallpaper metadata corrupts and the downstream batch stops. A slightly stiff tone costs me far less than that.
Three things I checked before shipping
1. Measure match rate per language. Averaged across all languages, strong English and Indonesian numbers mask a weak Thai one. I missed this for about three weeks. Grouping by language surfaces it the same day.
2. Don't classify with a model. Ask Gemini to identify the output language and your detector inherits the same wobble you are trying to measure. Unicode script composition is deterministic and ran in under 0.1 ms per item.
3. Re-measure when the model behind the alias changes.gemini-flash-latest switched to Gemini 3.5 Flash on July 15 when it reached general availability, so the alias now points somewhere new. Every number here was taken after that switch, but the same thing can happen again. Sampling 100 requests per language weekly is enough to notice. For the budget side of that watch, I wrote up setting a per-user token budget.
Where to start
If you are running a multilingual app on a single English instruction right now, measure that instruction with countTokens across your target languages. It takes ten minutes. At 1.2x there is nothing to think about. At 2.8x there is.
Then add the BCP-47 line, send 100 requests, and put the match rates next to each other. If that is enough, you never have to build the translation step at all.
The whole exercise cost me half a day. In exchange, an entire translation pipeline I had been about to build turned out to be unnecessary. Days when the thing you didn't have to build goes up by one are quietly good days.
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.