I asked Claude Code to translate the same 92 missing strings nine times. Three times it had only the JSON files. Six times it also had ten tools I wrote for exactly this job.
I expected the first group to break my files. It broke nothing. And in the second group, two sessions out of six never called a single one of my tools.
Why I ran this
I maintain a JetBrains plugin for translation keys. It exposes ten tools to AI agents through the IDE's MCP server: list the missing translations, write them, check the files, rename a key, and so on.
My assumption was simple. An agent that edits locale files by hand will rewrite values it should leave alone and lose a {{name}} on the way. The tools exist to prevent that. I had never measured it, so I did.
The setup
A small React app with i18next, five namespaces, 54 keys in English.
| Locale | Keys present | Keys missing |
|---|---|---|
en (reference) |
54 | 0 |
fr |
44 | 10 |
es |
26 | 28 |
it |
0 | 54 |
That makes 92 values to write and 70 existing values that must not move. The missing ones include {{name}} placeholders, _one / _other plural pairs, <b> tags and a line break.
The prompt is the same every time, and it never mentions a tool:
This React app uses i18next; its translation files are in public/locales.
Translate the keys missing in fr, es and it from the English reference.
Each run is a fresh headless session on a fresh copy of the project:
claude -p "$PROMPT" \
--model sonnet \
--setting-sources project \
--strict-mcp-config \
--permission-mode acceptEdits \
--settings '{"sandbox":{"enabled":true,"autoAllowBashIfSandboxed":true}}' \
--allowedTools Read Edit Write Glob Grep \
--no-session-persistence \
--output-format stream-json --verbose
This is Claude Code 2.1.319 with Sonnet 5.5. --setting-sources project keeps my personal settings out of the session. The runs with tools add --mcp-config with the IDE's server, and the ten tool names to the allowed list.
After each run, a script compares every file with the starting point: values written, existing values changed, placeholders and tags against the English text. Time, cost and turns come from the session's own report.
Without the tools: it just worked
| Run | Written | Placeholders or tags lost | Existing values changed | Time | Cost | Turns |
|---|---|---|---|---|---|---|
| 1 | 92 / 92 | 0 | 1 | 57 s | $0.20 | 33 |
| 2 | 92 / 92 | 0 | 0 | 57 s | $0.19 | 8 |
| 3 | 92 / 92 | 0 | 0 | 85 s | $0.27 | 30 |
Every placeholder survived. Every tag survived. The plurals came in pairs. My assumption was wrong.
The agent read the files, then wrote again, in full, the eleven that needed a change. In one run it generated them with a Python script instead.
The one changed value is in run 1. The French file had login.title set to "Login". The agent replaced it, and said so in its summary:
Existing fr value changed:
fr/auth.jsonhadlogin.titleset to"Login", which was left in English. I changed it to"Se connecter".
It is probably right about the French. But I asked for the missing keys. In a real project, "Login" may be the word the product team chose, and one modified line in a diff of 205 added lines is easy to miss.
With the tools: less than half the time
In four of the six sessions that had the MCP server, the agent used the tools.
| Run | Written | Placeholders or tags lost | Existing values changed | Time | Cost | Turns |
|---|---|---|---|---|---|---|
| 1 | 92 / 92 | 0 | 0 | 25 s | $0.14 | 9 |
| 2 | 92 / 92 | 0 | 0 | 22 s | $0.13 | 9 |
| 5 | 92 / 92 | 0 | 0 | 25 s | $0.13 | 10 |
| 6 | 92 / 92 | 0 | 0 | 35 s | $0.15 | 15 |
On average that is 27 seconds against 66, and $0.14 against $0.22.
The reason is easy to see in the transcript. The agent never opens a JSON file. It asks for the list of what is missing, once per locale:
{"module":"project","sourceLocale":"en","locale":"fr","total":10,"items":[
{"key":"dashboard:welcome","source":"Welcome back, {{name}}!","pluralFromOther":false},
{"key":"pagination.page","source":"Page {{current}} of {{total}}","pluralFromOther":false}
]}
(I kept two of the ten items.) Then it sends the translations back, once per locale, and the plugin writes them. That is about 4,000 output tokens instead of about 10,000.
The write tool is also stricter than a file edit. I called it by hand to see what it refuses:
{"written":[],"rejected":{"auth:login.title":"Already filled and not stale: only a stale value is replaced"}}
{"written":[],"rejected":{"dashboard:welcome":"Placeholders differ from the source: Welcome back, {{name}}!"}}
The first line is the "Login" rewrite from above. Through the tool, it cannot happen. The second is a French value I sent without its {{name}}.
Two sessions out of six ignored the tools
In runs 3 and 4 the server was connected and the ten tools were in the session's tool list. The agent wrote the files itself anyway.
| Run | Tools called | Existing values changed | Time | Cost |
|---|---|---|---|---|
| 3 | none | 1 | 70 s | $0.23 |
| 4 | none | 1 | 67 s | $0.24 |
Both made the same "Login" rewrite, once as "Se connecter" and once as "Connexion".
As far as I can tell, this comes from how Claude Code handles MCP tools. It does not load their definitions up front. The four sessions that used my tools each ran a tool search that fetches them by name. The two other sessions never searched.
One line fixed it
I added a CLAUDE.md with a single line to the project:
For translation files (public/locales), use the i18n_* MCP tools instead of editing the JSON files.
Three more runs, same prompt. All three used the tools: 26 to 30 seconds, $0.13 each, nothing existing touched. Three runs prove little, but it went from four out of six to three out of three.
What 119 tools cost
The JetBrains MCP server in my IDE exposes 119 tools: database, debugger, terminal, git and the rest. Their definitions weigh about 236,000 characters. My ten tools are 9,146 of them.
I expected that to eat the context window. It does not. The first turn of a session holds 20,556 tokens without the server and 22,879 with it. That is 2,323 tokens for 119 tools.
It is the same mechanism as the section above. The tools are cheap because they are loaded on demand, and they get ignored because they are loaded on demand.
Where the agent alone did better
Two things, and both are on my list now.
Terminology. The existing French file keeps the word "trustee" in English. The existing Spanish file says "Panel" for Dashboard. The agent that read the files reused both, in all three runs. Through the tools, the agent only sees the English text of what is missing. Over the seven sessions that used the tools, six translated "trustee" on their own, as "fiduciaires" or "personnes de confiance", and three wrote "Panel de control" where the file already said "Panel". Nothing was refused, because the placeholders were intact.
Key order. In an empty file, the tools write the keys in alphabetical order: errors before login. The English file has login first. The agent alone copied the order of the reference.
Limits
- Twelve sessions, one model, a 54-key project. This is one afternoon of measurements, not a benchmark of Claude Code.
- In four of the five sessions that edited files, my sandbox refused two or three shell commands, which cost them a few turns. The one session without any refusal took 57 seconds and $0.20, so the gap remains.
- Small files help the agent alone. Reading and rewriting whole files grows with their size, while the tool returns only what is missing. I expect the gap to widen on a large project. I have not measured it.
- I did not grade the translations.
- The tools need a JetBrains IDE open on the project.
Try it
The plugin is I18n Support Plus, free and open source (MIT), for JetBrains IDEs. The tools need version 2025.2.6 or later.
1- In the IDE: Settings › Tools › MCP Server, check Enable MCP Server.
2- Connect Claude Code:
claude mcp add --transport http jetbrains http://127.0.0.1:64342/stream
3- Add the line above to your CLAUDE.md, then ask for the missing translations.
The documentation covers Codex, Cursor and the other clients. The code is on GitHub.
Update, November 7: I tested the fixes from the comments
A commenter pointed out that tool definitions are loaded on demand, so the agent chooses from the tool names and reads a description only after it has decided to look. Two fixes followed from that: put "use this instead of editing the files" in the tool descriptions, and expose fewer tools. I shipped the first one (PR #870) and reran the same prompt.
| Setup | Sessions that wrote through the tools |
|---|---|
| 10 tools, new descriptions, no CLAUDE.md line | 0 of 6 |
| Only the 2 translation tools exposed | 0 of 3 |
| Control, with the CLAUDE.md line | 3 of 3 (22 to 24 s, $0.12 to $0.13) |
In this setup the descriptions did nothing, and fewer tools did nothing either. Two sessions called i18n_missing_translations after writing the files themselves, to check their work. The project-level line is still the only thing that worked. Small sample, one model, and a different IDE build from the first run, so don't compare these numbers with the 4 of 6 above.
Another commenter suggested loading the tools up front instead of on demand. I haven't tested it yet.
And since version 1.20.0 (November 9), i18n_missing_translations returns the project glossary and the existing translations that share a word with the keys to translate, which targets the terminology drift described in "Where the agent alone did better".
One question to finish. If you have MCP servers connected to your agent, have you ever counted how often it calls them? Run the same prompt three times and count. I would like to see your ratio.
I build tools that cut the daily friction of front-end work. More on my site. Let's talk on LinkedIn.
Top comments (12)
The MCP docs back that reading: with tool search on (the default), only tool names and server instructions load at session start. The same page names the server-side fix for the two sessions that never searched. Server instructions "help Claude understand when to search for your tools." Your ten tools ride on the IDE's server, so that field belongs to JetBrains, and your CLAUDE.md line ends up doing its job.
Two other levers, each with a cost.
"alwaysLoad": trueon the server entry in your--mcp-configloads its tools upfront whateverENABLE_TOOL_SEARCHsays, but it works per server, so all 119 definitions come in. Per tool, a server can set"anthropic/alwaysLoad": truein that tool's_meta. Does the JetBrains plugin API let you set_metaon your ten? That would load only yours and skip the search step.Thanks, that's the most precise explanation so far, and it matches what I saw: the two sessions that skipped the tools never ran a tool search, so they only had names to go on. I haven't tried alwaysLoad yet. Loading all 119 definitions would cost a lot more than the ~2,300 tokens I measured, so the per-tool _meta option is the interesting one. I don't know yet whether the plugin API lets me set _meta on the tools I register. I'll check, and if it does, I'll run the same 9 sessions with only my ten tools always loaded and no CLAUDE.md line, then post the numbers here.
Follow-up, as promised.
Does the JetBrains plugin API let me set _meta on my ten tools? No. The IDE's MCP server builds every tools/list entry with _meta left empty (checked in 2025.3 and 2026.1), and a plugin's tool descriptor has no field for it. So the per-tool "anthropic/alwaysLoad" is out of my hands; it would need JetBrains to expose it.
So I ran the 9 sessions with your other lever: "alwaysLoad": true on the server entry, no CLAUDE.md line, same prompt and project, Sonnet.
The cost is the one you predicted: every tool of the IDE's server loads upfront (73 in this WebStorm build), so the first turn goes from ~16k to ~48k tokens of context, in every session of the project, translation or not.
The CLAUDE.md line gets the same 3/3 at $0.12-0.13 with no extra cost, so it stays my recommendation, and alwaysLoad is now documented as the fallback when you can't touch the project file: github.com/IBRAHIMDANS/i18nSupport...
Really useful measurement — the 2-in-6 miss rate is the interesting number here, more than the speed win.
One thing your results point to: the CLAUDE.md line worked because it is in context from turn zero, while the tool definitions are only loaded on demand. So at the moment the model decides how to do the job, it is choosing mostly from tool names, not from what the tools actually do. That makes the name and the first sentence of the description do most of the work — "use this instead of editing the locale JSON files" belongs in the description itself, not just in CLAUDE.md.
The other lever is count. Ten tools for one job means ten chances for the model to decide none of them is the obvious entry point. A single entry-point tool that lists the missing keys and writes them in one call would probably get picked more reliably than ten granular ones, at the cost of some flexibility — worth an A/B run if you repeat the experiment.
Thanks, that matches what I saw. You are right about the descriptions: none of them says "use this instead of editing the locale files". They describe what each tool does, not when to prefer it over a plain edit. I will move that sentence from CLAUDE.md into the descriptions of i18n_missing_translations and i18n_write_translations, then rerun without the CLAUDE.md line to see whether it is enough on its own.
On a single entry point: the plugin has no model, so the agent does the translating. That means two calls at least, one to list what is missing and one to write it. But the A/B is cheap to run: same prompt, only those two tools allowed, against the full ten. I will post the numbers here.
@agentwareshq
I ran it. Same prompt, same setup, and the new descriptions now open with "Use this instead of editing the translation files...".
So in this setup the description did nothing, and fewer tools did nothing either. That fits your first point better than your fix. With deferred tools, the model decides from the names, and it reads a description only once it has already chosen. The project-level line is what it sees at turn zero, and that is still the only thing that worked.
Small sample, one model, and a different IDE build from the first run, so I would not compare these numbers to the 4 of 6 from the article. But the control makes the point. Thanks for pushing me to measure it.
PR #870
The tool only shows the agent the English text, so it never sees that the French file keeps "trustee" in English. What if the list of missing keys also came with a few existing translations from that file?
Good catch, and it explains part of the terminology drift I mentioned: the tool gives the agent the source text but not the decisions already made in the target file. Returning a few existing translations from that file next to the missing keys would be cheap to add, and it's a better fix than asking the agent to go read the JSON on its own. I've noted it for the plugin. Do you think a short sample would be enough, or would you rather see a glossary of terms that stay untranslated?
the two runs that skipped the tools are the number I would chase. a tool description is only read when the tool is listed, so a one line pointer in the project file saying to use them first gets read on every run. did you try that, and did the miss rate move
Yes, that's the fix that worked. With one line in CLAUDE.md pointing at the i18n_* tools, 3 of 3 runs used them, and the control in my rerun gave 3 of 3 again. Moving the same sentence into the tool descriptions did nothing (0 of 6), and so did exposing only the two translation tools (0 of 3). Details are in my reply to the first comment. Small samples, but for now the project file is the only lever that moved the miss rate.
Tool names that match the task wording (say translate_missing_strings) might help too, since the model picks tools partly by how closely the name fits the request.
Good point, though in this case the name was already close: the prompt said "translate the keys missing" and the tool is i18n_missing_translations. The two sessions that skipped the tools never ran a tool search at all, so they never got to the name. My guess is that the decision to edit the files by hand happens before name matching. A description that says "instead of editing the files" might reach it earlier. That is the next run.