skill
Tool-Translate-OpenRouter
Translate PDF documents between any languages via OpenRouter. Preserves layout, formulas, tables and images. Desktop GUI + CLI, cross-platform.
Set up
Skills are installed by placing their folder where your agent loads skills.
The download comes from github.com/hoangtung386/Tool-Translate-OpenRouter itself, not from our server, so what you get is exactly what its author published.
Install instructions come from the README
README
synced Oct 26, 2026The tool analyses the layout of each page, protects formulas and code, translates the prose, then puts the content back in the exact position in the original document — it does not flatten your PDF into a single wall of text.
There are two ways to use it: the desktop application for end users, and the command line for automation or integration into an AI agent.
Table of contents
- Why this project exists
- Highlights
- Installation
- Configuration
- Usage
- The three translation engines
- Translating text in images
- Fixed terminology
- Supported languages
- Using it as an Agent Skill
- Development
- Privacy
- License and attribution
Why this project exists
The PDF processing core in pdf2zh/ originates from
PDFMathTranslate 1.9.11.
The original author forked and Vietnamese-localised it into
VI-Translate.
In real use I found that project still missing a lot, and disagreed with some of its design decisions. So I forked it into this project to redesign the system and overcome four limitations:
| Limitation in the original | How this project addresses it |
|---|---|
| Windows-only | The entire platform-specific layer — config directory, font registration, serif selection, API-key store — has been rewritten per operating system. It runs from source on Linux and macOS as well. |
| Limited to 36 languages | The hard-coded list is gone. It accepts any language code; when a font cannot draw that script it reports a clear error instead of producing an all-tofu PDF. |
| Only via Codex / Claude Code / Copilot | Added the OpenRouter engine: one API key and it works, no agent required. You get the latest models from many providers, including free models. |
| Cannot translate text baked into images | Added a VLM path: a vision model reads the text in illustrations, diagrams and image-style slides and translates it. |
Highlights
Preserves layout
- Preserves the position of paragraphs, formulas, tables, figures, the table of contents and references.
- Formulas keep their original glyphs, represented only by placeholders during translation. A translation that breaks a placeholder is rejected and never cached.
- Tables are translated cell by cell, keeping the grid, background and borders.
- Table-of-contents, index, symbol-list and reference pages keep their layout instead of being reflowed.
- Bold, italic and bold-italic text cross the translator as validated markers.
Translation
- OpenRouter is the default engine, with a fallback model when the primary fails.
- Distinguishes transient from permanent errors: a wrong key or a non-existent model fails in seconds, not after minutes of useless retries.
- A SQLite cache keyed by model, endpoint and prompt version — switching models re-translates for real instead of returning a stale result.
- A fixed-terminology table keeps one term translated the same way on every page.
- One bad segment does not ruin the whole document: the original is kept and the count of untranslated segments is reported.
Desktop application
- Drag and drop multiple PDFs or whole folders.
- A Stop button cancels mid-run, safely.
- Choose an output folder, or leave the default next to the source file.
- The API key is stored in the OS secret store (Windows Credential Manager, macOS Keychain, Secret Service), not a plain text file.
- One failed file does not break the queue.
- Notifies you when a new release is out.
Installation
Running from source (Windows, Linux, macOS)
git clone https://github.com/hoangtung386/Tool-Translate-OpenRouter.git
cd Tool-Translate-OpenRouter
python -m venv .venv
# Linux / macOS
.venv/bin/python -m pip install -r requirements-app.txt
.venv/bin/python app/gui.py
# Windows
.venv\Scripts\python.exe -m pip install -r requirements-app.txt
.venv\Scripts\python.exe app\gui.py
If you only need the command line, requirements.txt is enough and lighter:
.venv/bin/python -m pip install -r requirements.txt
[!NOTE] The first run downloads the layout model (~70 MB) and a Unicode font via BabelDOC. To run fully offline, pre-fetch them with
python scripts/fetch_assets.py, or pointPDF_TRANSLATE_MODELandNOTO_FONT_PATHat existing files.
Packaging a Windows build
.\build.ps1
Creates dist\PDFTranslate-windows.zip with the model and font bundled, plus a
SHA256 sum. A build missing assets is flagged DEV_BUILD and the release
process refuses to publish it.
[!IMPORTANT] Only a Windows packaging script exists for now. On Linux and macOS, run from source as above — every feature works, there just is no prebuilt executable yet.
Verifying a downloaded build
The application is not code-signed, so Windows SmartScreen will warn you. Before choosing More info → Run anyway, verify it yourself:
# Compare against SHA256SUMS.txt attached to the Release
Get-FileHash PDFTranslate-windows.zip -Algorithm SHA256
# Or verify the cryptographic provenance that the file was built from this exact commit
gh attestation verify PDFTranslate-windows.zip -R hoangtung386/Tool-Translate-OpenRouter
Configuration
You need an API key from openrouter.ai/keys. There are many free models, enough for short documents.
Precedence, highest first:
- The input field in the app's Settings, or a command-line flag
- An environment variable exported in the shell
- A
.envfile in the project directory — read by both the CLI and the desktop app - The defaults in
pdf2zh/defaults.py
cp .env.example .env
| Variable | Default | Effect |
|---|---|---|
OPENROUTER_API_KEY | — | Required for the openrouter engine and image translation |
OPENROUTER_MODEL | google/gemma-4-31b-it:free | Primary model |
OPENROUTER_MODEL_2 | — | Fallback model, used only when the primary fails |
OPENROUTER_BASE_URL | OpenRouter | Change the endpoint (proxy / self-host) |
OPENROUTER_REASONING | true | Enable reasoning mode if the model supports it |
OPENROUTER_VISION_MODEL | google/gemma-4-31b-it:free | Model that reads text in images |
PDF_TRANSLATE_GLOSSARY | — | Fixed-terminology JSON file |
PDF_TRANSLATE_MODEL | — | A prebuilt layout model, for offline use |
NOTO_FONT_PATH | — | A prebuilt Unicode font, for offline use |
PDF_TRANSLATE_CACHE | ~/.cache/pdf2zh | Where the cache is stored |
PDF_TRANSLATE_BACKEND | auto | cpu, cuda or dml for the layout model |
Only the variables above are read; everything else in .env is ignored.
Usage
Desktop application
- Add documents — drag and drop files or a folder, or click Choose file /
Choose folder. On Windows you can also drop straight onto
PDFTranslate.exe. - Pick a language — defaults to translating into Vietnamese.
- Click Translate — watch per-page progress; click Stop to cancel.
Results are saved in a translated folder next to the source file:
TaiLieu/
├── document.pdf
└── translated/
└── document-vi.pdf
To use another location, set it in the Save to field. If the destination
folder cannot be written (read-only network drive, locked folder), the result
is silently redirected to ~/Documents/PDF Translate instead of erroring.
By default it does not overwrite previous results; enable Overwrite already translated files to re-translate.
Command line
export OPENROUTER_API_KEY="sk-or-..."
python scripts/translate_pdf.py INPUT.pdf --output-dir OUT
| Flag | Default | Effect |
|---|---|---|
--output-dir | — | Output folder (required, unless using --emit-segments) |
--target-language | vi | Target language |
--source-language | auto | Source language |
--engine | openrouter | openrouter, google or handoff, with an optional :model |
--openrouter-model | from .env | Choose the model for this run |
--pages | all | Page range counted from 1, e.g. 1,3-5 |
--threads | 4 | Number of parallel translation threads, max 8 |
--glossary | — | Fixed-terminology JSON file |
--segments | — | For handoff only: a JSONL file of translations to rebuild the PDF |
--emit-segments | — | For handoff only: write out the untranslated segments |
--translate-images | off | Translate text baked into embedded images via VLM |
--vlm-image-model | from .env | Vision model for this run |
--ignore-cache | off | Skip the cache and translate from scratch |
--overwrite | off | Allow overwriting existing results |
Choosing a different model for one run:
python scripts/translate_pdf.py INPUT.pdf \
--engine openrouter:anthropic/claude-opus-5 --output-dir OUT
The model is any slug on openrouter.ai/models. Because billing is per token, pick a smaller model for long documents.
The three translation engines
| Engine | Needs | Best when |
|---|---|---|
openrouter (default) | One API key | High quality, no agent needed, translates any language pair |
google | Nothing | A quick draft, or when you have no key yet |
handoff | A running AI agent | Specialised documents needing context-aware translation |
A real difference, with the word conduction in a heat-transfer document:
| OpenRouter / Handoff | |
|---|---|
| "Electrical conduction occurs when two objects touch directly" | "Heat conduction occurs when two bodies touch each other directly" |
[!WARNING] The
The handoff workflow
# 1. Extract the segments to translate
python scripts/translate_pdf.py INPUT.pdf --engine handoff --emit-segments segments.jsonl
# 2. Have an agent translate segments.jsonl into translations.jsonl ({"src","dst"})
# 3. Rebuild the PDF
python scripts/translate_pdf.py INPUT.pdf --engine handoff \
--segments translations.jsonl --output-dir OUT
Translating text in images
Text burned into an image (illustrations, diagrams, image-style slides) is not a
PDF glyph, so the text engines cannot reach it. Enable --translate-images, or
tick Translate images with VLM in the app, to let a vision model read and
translate it.
[!IMPORTANT] This is a caption, not an in-place replacement. The model reads the text in the image and translates it, then the translation is drawn as a band at the bottom of the image; the original text in the image is kept. The image is resized back to its original dimensions so the page layout is unchanged, but the caption band may cover the lower part of the picture.
In-place replacement needs the model to return the coordinates of each line — that is planned but not available yet.
Images that fill nearly the whole page (scanned PDFs) are skipped, leaving room for the OCR path. The remaining images are translated in parallel and have their own on-disk cache.
Fixed terminology
Some terms must be translated the same way on every page, or tables will read as
broken. Create a JSON file from the template
glossary.example.json:
{
"vi": {
"Designation": "Tên gọi",
"Unit": "Đơn vị"
}
}
python scripts/translate_pdf.py INPUT.pdf --glossary glossary.json --output-dir OUT
Only a segment that fully matches a term is replaced; a term inside a sentence is still left to the engine, because the engine has the context to inflect it correctly.
Supported languages
It accepts any language code (vi, en, ja, zh-CN, pt-BR…). The
translation is done by the model; the real limit is whether a font can draw
that script.
- Latin script, including Vietnamese with full diacritics: works well with the bundled font.
- Chinese, Japanese, Korean, Arabic, Hebrew, Thai, Indic scripts, Cyrillic, Greek: the bundled font has no glyphs. The tool checks before translating and reports an error, instead of producing an all-tofu PDF for you to discover later.
If you have a suitable font, just point NOTO_FONT_PATH at it and it works
immediately:
NOTO_FONT_PATH=/path/to/NotoSansJP-Regular.ttf \
python scripts/translate_pdf.py INPUT.pdf --target-language ja --output-dir OUT
For Vietnamese, the tool auto-detects a serif family available on the machine to get all four real styles — Times New Roman on Windows, Times/Georgia on macOS, Liberation Serif or DejaVu Serif on Linux. If none, it uses the bundled Unicode font and fakes bold/italic.
Using it as an Agent Skill
The repo follows the Agent Skills standard and works
with Codex, Claude Code, GitHub Copilot and any coding agent that supports
SKILL.md.
npx skills add hoangtung386/Tool-Translate-OpenRouter -g --all
Use $pdf-translate to translate this PDF into Vietnamese.
See the full workflow in SKILL.md.
Development
Dig deeper: docs/ARCHITECTURE.md (how the pipeline works and which file is dangerous), references/preservation-rules.md (the layout-preservation contract — changes need regression tests).
Setting up the environment
git clone https://github.com/hoangtung386/Tool-Translate-OpenRouter.git
cd Tool-Translate-OpenRouter
python -m venv .venv
.venv/bin/python -m pip install -r requirements-dev.txt # Windows: .venv\Scripts\python.exe
requirements-dev.txt pulls in the app, the packaging dependencies, pytest and
ruff, so a single install covers everything you need for development. Copy
.env.example to .env if you want to try the default engine; tests need no
key — tests/conftest.py strips credentials from the environment and points the
cache at a temporary directory, so a test neither touches the network nor your
real cache.
The three check commands
ruff check . # lint
ruff format --check . # compare against the format standard (use `ruff format .` to fix)
pytest # 177 tests, none skipped
Run them on your machine before committing — this project has no remote CI.
Before opening a pull request
ruff check .andpytestare green.- A new test has a name that states the behaviour, not the function under test.
test_a_bad_key_costs_one_request_per_model_not_forty_eighttells a reviewer what broke;test_do_translate_2does not. - Comments say why, not what. The code already says what.
- No new default value, version number or engine name written in more than one
place. See
pdf2zh/defaults.pyandpdf2zh/_version.py.
Where to start
Read docs/ARCHITECTURE.md first. In short:
pdf2zh/converter.py is the one place a reasonable-looking change can silently
ruin page layout, and the test suite does not compare layout — everything else
is ordinary code.
Releasing
- Bump
__version__inpdf2zh/_version.pyandversion:inCITATION.cff.tests/test_packaging.pyfails if they disagree. - Build the Windows bundle with
.\build.ps1— it createsdist\PDFTranslate-windows.zipwith the model and font bundled, plus a SHA256 sum. A build missing assets is flaggedDEV_BUILD. - Commit, then tag
vX.Y.Zand push the tag. Publish the release manually on GitHub if you want to ship it.
The in-app update check reads the tag from the GitHub API and the download link is a fixed asset name, so if you publish, set the asset name correctly — a wrong name will break the update button.
Testing network-touching code
Don't call the real network. Stub the transport and assert on what was sent:
post = mock.Mock(return_value=_response(429))
with mock.patch.object(type(translator), "session", mock.Mock(post=post)):
...
self.assertEqual(post.call_count, 3) # retried, not retried forever
tests/test_openrouter_translator.py has the full set of helpers.
Pre-commit (optional)
pre-commit install # runs ruff on staged files
pre-commit run --all-files
Privacy
The tool sends document text to a third party for translation. Who receives it depends on the engine you choose:
| Engine | Receives the text | Notes |
|---|---|---|
openrouter (default) | OpenRouter, and through it the model provider you pick | Subject to OpenRouter's terms and privacy policy, plus the specific model's terms. Billed per token on your own API key. |
google | translate.google.com | Unofficial web endpoint. Google does not sponsor or endorse this project; access and usage terms remain Google's. |
handoff | Not sent over the network | Text is written to a local JSONL file for the agent running on your machine. |
When using --translate-images, embedded images are additionally sent to the
model configured by OPENROUTER_VISION_MODEL.
No other data is transmitted. The update check only calls the GitHub releases API and sends no document content. The API key is stored in the OS secret store when one is available; otherwise it sits in the config file with owner-only read permissions.
License and attribution
Released under AGPL-3.0. If you redistribute this tool or offer it as a service over a network, you must provide the corresponding source code.
Derivative works
The pdf2zh/ directory is the PDF processing core developed from
PDFMathTranslate 1.9.11,
retaining the PDF parsing, formula handling, layout detection, rendering and
caching. Licensed under AGPL-3.0.
This project forked from VI-Translate by Lê Ngọc Gia Huy (@huyg.ai), who did the Vietnamese localisation and desktop-app packaging.
BabelDOC 0.2.33 provides the pinned layout model and font resources. Licensed under AGPL-3.0.
Bundled fonts
| Font family | License | Source |
|---|---|---|
| Be Vietnam Pro | SIL Open Font License 1.1 | fonts.google.com |
| JetBrains Mono | SIL Open Font License 1.1 | fonts.google.com |
| Go Noto Kurrent | SIL Open Font License 1.1 | obtained via BabelDOC |
The OFL license text ships with each font family in app/fonts/.