Claude Translator
This is the pipeline that runs www.conveythis.com itself — a 238-page Astro site, live in 55 languages. We extracted it, documented every failure mode it cost us to find, and published the whole thing. It is not a trial and not a teaser: there is no key to buy, no quota, and no account.
Two design decisions
How it works
Two decisions define it. First, translations are spliced into the built HTML by byte offset and the document is never re-serialised from a DOM — so inlined critical CSS, the LCP element, asset hashes and the width/height attributes that hold CLS all carry over untouched. That is why locale pages match the source language on Core Web Vitals rather than merely resembling it. Second, the unit of translation is the whole block, not the text node, because roughly a fifth of a typical site's text nodes are split by inline markup and translating the fragments separately breaks grammar in any language that reorders or inflects.
The result is proved rather than asserted: eight gates and an exhaustive SEO audit over every page, including one gate that exists purely because coverage metrics are structurally blind to extraction bugs.
Verification
Does it actually hold?
Fair question, and it is checkable rather than something you have to take our word for. doctranslator.com/fr is a large Astro site whose French pages were built by this pipeline. Compare it against the English original and the markup is byte-identical:
2,743
tags, in identical sequence
2,031
class attributes, all matching
+0.74%
page weight — French is longer
Nothing structural moved, so there is nothing for the browser to lay out differently. Check it yourself — it takes about ten seconds:
curl -s https://doctranslator.com/ | grep -o 'class="[^"]*"' > en.txt
curl -s https://doctranslator.com/fr | grep -o 'class="[^"]*"' > fr.txt
diff en.txt fr.txt && echo "markup identical — only the text changed"And the scores
| English source | French, from this tool | A11y / Best practices / SEO | |
|---|---|---|---|
| Desktop | 100 | 100 | 100 / 100 / 100 |
| Mobile | 98 | 98 | 100 / 100 / 100 |
PageSpeed Insights performance scores, measured 25 August 2026. Accessibility, best practices and SEO were 100 on every run of both pages, in both strategies.
These are medians, deliberately. Lighthouse scores move: five consecutive runs of the same English mobile page returned 98, 98, 88, 98, 98, with largest-contentful paint swinging between 1.8s and 3.2s. That is network and CDN variance rather than page quality. If you measure once and get 91, that is why — run it a few times and compare the two languages against each other rather than against a number on a marketing page.
And the language
Markup identity is measurable. Whether the Spanish is any good is a different question, and until 2.0 the pipeline had no answer to it — the tests checked request shapes and scaffolding, and nothing scored a translation.
node scripts/i18n/tqa.mjs --lang es --dry # sample size and cost, no API call
node scripts/i18n/tqa.mjs --lang es,fr,de # scorecard + per-locale JSON
node scripts/i18n/tqa.mjs --lang es --repeat # judge the same sample twiceIt scores a stratified sample on the MQM error typology at its conventional weights — minor 1, major 5, critical 10 — and weights the sample by how often each string appears, so the header everyone reads counts for more than a one-off footnote. Three things keep the number from being decorative:
- The judge is a different model from the translator
- Models score their own output generously. If no second provider key is configured it uses the same one and says so, in the run and in the report.
- The sample is seeded, and the variance is reported
- A seed reproduces a score exactly;
--repeatscores the same sample twice and prints the gap. A quality number nobody can re-derive, quoted without its noise, is a marketing number. - A unit the judge cannot assess is excluded, not counted as clean
- An early version printed 100.00 out of 100 from a sample where every single unit had failed to parse. It now refuses to report a score at all in that case.
Read it as a comparison — between locales, between models, before and after a prompt change — not as a grade. It is one model’s assessment of another’s work rather than a human review, and the report says so on its face.
Decision three
Terminology is not a per-sentence choice
Identical strings were always consistent — units are keyed by a hash of the source text, so a nav label translated once is reused on every page and across runs. What that cannot do is hold a term inside varying sentences: “Dashboard” in two different paragraphs is two hashes, two batches, two stateless requests, and nothing compared them.
[
{ "source": "Acme", "rule": "keep", "matchCase": true },
{ "source": "Dashboard", "rule": "translate",
"targets": { "es": "Panel de control", "de": "Übersicht" } }
]matchCase is the answer to a question we were asked directly: can it tell Apple the company from apple the fruit? A flat list of names cannot express that. Whole-word, case-sensitive rules can.
| source | spanish | why |
|---|---|---|
| Apple announced a device | Apple anunció un dispositivo | capital A — protected, and now verified |
| An apple a day | Una manzana al día | lowercase — translated normally |
| Applesauce is on sale | La compota está en oferta | whole-word — never matches inside a longer word |
The same release tells the model what a string is. It used to receive the text and nothing else, so a button label and a body paragraph were indistinguishable — which made the prompt’s own instruction to keep button labels short unenforceable. A <button>, a heading, a form label and a meta description now each say so.
It deliberately does not demand the imperative for buttons: German UI prefers a verbal noun and French the infinitive, so it asks for whatever construction that language actually puts on a button. Brand names you already list are folded in automatically as case-sensitive keep rules, which also brings them under a verification gate that checks they survived. Nothing checked that before.
Locale conventions
Numbers get localized. Prices never get converted.
Writing 1,234.56 to a German reader is one of the most visible marks of a machine translation. Models are unreliable at separator conventions, so the model is told to leave numbers alone and Intl reformats them afterwards, deterministically.
| source | becomes | where |
|---|---|---|
| 1,234.56 | 1.234,56 | de |
| 1,234.56 | 1 234,56 | fr |
| $5 | 5,00 $US | fr — placement and spacing, not the amount |
| 50% | 50 % | fr — with a non-breaking space |
It formats. It never converts.
no config option for itA price is a commercial commitment, and converting one at a rate baked into a build — stale the day after it is written — is how a translation tool starts publishing wrong offers in markets nobody is watching. Every monetary amount found is written to a report instead, so a human prices each market deliberately.
A numeric-integrity gate backs that up by failing the build if a number’s value changes between source and translation. A model that quietly ships $39 where the source said $49 passes every other check: identical markup, matching placeholders, plausible length, fluent Spanish.
Deliberately left alone: version numbers, times, IP addresses, ISO dates, phone numbers and any ungrouped number — during development a greedy pattern turned 192.168.1.1 into 1.921.681,1, and that case is now a test. One result that looks like a bug and is not: Spanish does not group four-digit numbers, so 1,234.50 is correctly 1234,50 in es and 1.234,50 in de.
Honest routing
Which one do you actually need?
Static substitution and a runtime layer solve different problems. Picking the wrong one costs you a weekend, so here is the honest split.
You have a static build, content changes on a release cadence, and you want to own the HTML outright.
Claude Translator
Free, self-hosted, AGPL-3.0. Your own API key, your own files, no account.
View on GitHub →Your content changes daily, lives in a CMS, is user-generated, or sits behind a login or checkout.
ConveyThis
A managed runtime layer. No build step to hook, no re-run on every edit, a visual editor and human review.
See pricing →What you need translated is documents rather than pages — PDF, DOCX, XLSX, PPTX.
DocTranslator
Whole files, layout and tables preserved, 100+ languages.
Go to DocTranslator →Quickstart
Install it
Repository
https://github.com/ConveyThis/claude-translator
AGPL-3.0 · Node ≥ 20 · one dependency (parse5) · open on GitHub →
You need
- Node.js 20 or newer
- A site that builds to static HTML — it reads your build output, not your source
- An API key for whichever model you choose — or none at all, if you run one locally
As a Claude Code skill
Clone it into your skills directory, then ask Claude to localize the site. It follows the bundled instructions, including the failure modes that cost us real money to find — and the rules for when this is the wrong tool entirely.
git clone https://github.com/ConveyThis/claude-translator.git \
~/.claude/skills/claude-translatorIn any project
One command. init copies the pipeline into scripts/i18n/, writes a config, declares parse5 and adds the derived i18n/ paths to your .gitignore. It never overwrites anything without --force, and it prints every file it touched.
cd your-project
npx claude-translator init
npm install # parse5, the only dependencyThe scripts land in your repository rather than staying in node_modules on purpose — they resolve paths from the project they sit in, they are short enough to read, and the licence exists so you can change them.
Installing without npx
git clone https://github.com/ConveyThis/claude-translator.git
cp -r claude-translator/scripts your-project/scripts/i18n
cp claude-translator/i18n.config.example.json your-project/i18n.config.json
cd your-project && npm install --save-dev parse5Configure
The smallest config that runs. buildDir is where your generator writes HTML; baseUrl is your canonical origin, no trailing slash.
{
"buildDir": "dist",
"baseUrl": "https://example.com",
"provider": "anthropic",
"glossary": "glossary.json",
"locales": [
{ "hreflang": "es", "pathCode": "es", "nativeLabel": "Español" },
{ "hreflang": "de", "pathCode": "de", "nativeLabel": "Deutsch" }
]
}echo "ANTHROPIC_API_KEY=your-key-here" >> .env # and gitignore itRun it
| npm run build | your normal build — source language only |
| node scripts/i18n/extract.mjs | find every translatable unit |
| node scripts/i18n/translate.mjs --lang es,fr | translate into the memory |
| node scripts/i18n/build-locales.mjs --lang all | write the localized pages |
| node scripts/i18n/verify.mjs --lang all | eight gates — exits non-zero on failure |
| node scripts/i18n/audit-seo.mjs | canonicals, hreflang, JSON-LD, sitemaps |
| node scripts/i18n/tqa.mjs --lang es --dry | optional — MQM quality score, cost first |
Then deploy the build directory exactly as you deploy it today — the localized pages are ordinary files sitting next to your existing ones. Full options, every failure mode, and the reasoning behind each design decision are in the README.
Models
Bring your own model — including a local one
The translation step talks to a model through a small adapter, and three ship with it: Claude (the default), Gemini, and any OpenAI-compatible endpoint. That last one covers most of the field — OpenAI, Azure, Groq, DeepSeek, Mistral, OpenRouter, Together, Fireworks — and it also covers Ollama, LM Studio and vLLM.
Which means three lines of config run the entire pipeline on your own hardware:
{
"provider": "openai",
"apiBaseUrl": "http://localhost:11434/v1",
"model": "qwen2.5:14b"
}No key, no quota, and nothing leaves the machine — which matters if the content is confidential. Anything not on that list is an adapter file with two functions in it.
Limits
Where the free tool stops
Six real limits. None of them is a crippled feature — they are the shape of the approach, and the scripts tell you when you hit one instead of failing quietly.
- Client-side hydration
- Islands and framework payloads re-render in the browser, over the substituted HTML. The extractor counts the affected pages so you find out in seconds, not after a full run.
- Documents
- Linked PDFs, DOCX and XLSX stay in the source language — the pipeline only ever touches HTML.
- Churn
- The memory is keyed by source hash, so it can tell you what share of your site changed since the last run. High churn means paying to re-translate, repeatedly.
- Editing a translation
- Find the hash in the memory file, edit the string, rebuild. There is no editor, no reviewer and no workflow.
- Volume
- You pay your own model provider directly, at their rate, with your own key — or nothing at all, if you run the model yourself.
- Modified network use
- AGPL-3.0 §13 obliges you to publish modifications if you run a changed copy as a service for other people. Running it unmodified, or modified internally, is unrestricted.
The first four are the ones people actually hit, and they are all the same shape: something has to happen after the build, which a build-time tool cannot do. That is what the managed product is for, and the free plan is 5,000 words with no card, which is enough to find out whether it helps. You do not need it to use anything on this page.
Attribution
What it puts on your pages
Localized pages carry a <meta name="generator"> tag and one HTML comment naming ConveyThis — about 150 bytes, no request, no script and no link. It is the same mechanism Astro, Hugo and WordPress use. Two config keys remove it, and nothing in the repo checks whether you did.
If you would rather show a visible credit, set credit.visibleLink to true and place the slot wherever you want it on the page. Nothing is asked of you for it and nothing is given in return — it exists because some people want to credit the tools they use, and for no other reason.
The credit link is rel="nofollow", deliberately. Not because it is paid — it is not — but because it is a link a build script would otherwise add across every page of a site, and sitewide links that appear through tooling rather than editorial choice are the shape Google's link-scheme guidance is aimed at. It is worth referral traffic, not backlinks — anyone telling you otherwise is selling you a penalty.
Commercial licence
The code is AGPL-3.0. Localizing your own sites and shipping the output is unrestricted — the licence covers the software, not the HTML it writes. It only bites if you run a modified copy as a network service for other people. If that is you and you cannot publish your changes, write to [email protected].
Questions
The questions people actually ask
Is this really free? What is the catch?
Do I need a ConveyThis account?
What does a run actually cost?
Can I use my own model, or run it completely offline?
Will the translated pages be slower than the originals?
Does it work with my framework?
What about React components or Astro islands that hydrate?
Can it keep my terminology consistent, and tell a brand name from an ordinary word?
I already use it. What changes when I upgrade to 2.0.0?
Can I fix a translation I do not like?
What happens when I edit a page or add a new one?
Will the translated pages get indexed by Google?
Can I use it commercially? What does AGPL-3.0 mean for me?
Is the ConveyThis attribution required?
How is this different from ConveyThis itself?
Translation, far more than just knowing languages, is a complex process.
By following our tips and using ConveyThis , your translated pages will resonate with your audience, feeling native to the target language.
While it demands effort, the result is rewarding. If you're translating a website, ConveyThis can save you hours with automated machine translation.
Start with ConveyThis on our free plan — no credit card required!
