Skip to content
Conduit

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

Skills do not use an MCP config. Follow the steps in the README below.

README

synced Oct 26, 2026

The 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

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 originalHow this project addresses it
Windows-onlyThe 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 languagesThe 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 / CopilotAdded 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 imagesAdded 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 point PDF_TRANSLATE_MODEL and NOTO_FONT_PATH at 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:

  1. The input field in the app's Settings, or a command-line flag
  2. An environment variable exported in the shell
  3. A .env file in the project directory — read by both the CLI and the desktop app
  4. The defaults in pdf2zh/defaults.py
cp .env.example .env
VariableDefaultEffect
OPENROUTER_API_KEY—Required for the openrouter engine and image translation
OPENROUTER_MODELgoogle/gemma-4-31b-it:freePrimary model
OPENROUTER_MODEL_2—Fallback model, used only when the primary fails
OPENROUTER_BASE_URLOpenRouterChange the endpoint (proxy / self-host)
OPENROUTER_REASONINGtrueEnable reasoning mode if the model supports it
OPENROUTER_VISION_MODELgoogle/gemma-4-31b-it:freeModel 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/pdf2zhWhere the cache is stored
PDF_TRANSLATE_BACKENDautocpu, cuda or dml for the layout model

Only the variables above are read; everything else in .env is ignored.

Usage

Desktop application

  1. 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.
  2. Pick a language — defaults to translating into Vietnamese.
  3. 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
FlagDefaultEffect
--output-dir—Output folder (required, unless using --emit-segments)
--target-languageviTarget language
--source-languageautoSource language
--engineopenrouteropenrouter, google or handoff, with an optional :model
--openrouter-modelfrom .envChoose the model for this run
--pagesallPage range counted from 1, e.g. 1,3-5
--threads4Number 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-imagesoffTranslate text baked into embedded images via VLM
--vlm-image-modelfrom .envVision model for this run
--ignore-cacheoffSkip the cache and translate from scratch
--overwriteoffAllow 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

EngineNeedsBest when
openrouter (default)One API keyHigh quality, no agent needed, translates any language pair
googleNothingA quick draft, or when you have no key yet
handoffA running AI agentSpecialised documents needing context-aware translation

A real difference, with the word conduction in a heat-transfer document:

GoogleOpenRouter / Handoff
"Electrical conduction occurs when two objects touch directly""Heat conduction occurs when two bodies touch each other directly"

[!WARNING] The google engine calls an unofficial web endpoint, not an API. It can break at any time Google changes the page, and limits each segment to 5,000 characters — a longer segment will error out and keep the original text rather than be silently truncated.

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 . and pytest are 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_eight tells a reviewer what broke; test_do_translate_2 does 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.py and pdf2zh/_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

  1. Bump __version__ in pdf2zh/_version.py and version: in CITATION.cff. tests/test_packaging.py fails if they disagree.
  2. Build the Windows bundle with .\build.ps1 — it creates dist\PDFTranslate-windows.zip with the model and font bundled, plus a SHA256 sum. A build missing assets is flagged DEV_BUILD.
  3. Commit, then tag vX.Y.Z and 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:

EngineReceives the textNotes
openrouter (default)OpenRouter, and through it the model provider you pickSubject to OpenRouter's terms and privacy policy, plus the specific model's terms. Billed per token on your own API key.
googletranslate.google.comUnofficial web endpoint. Google does not sponsor or endorse this project; access and usage terms remain Google's.
handoffNot sent over the networkText 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 familyLicenseSource
Be Vietnam ProSIL Open Font License 1.1fonts.google.com
JetBrains MonoSIL Open Font License 1.1fonts.google.com
Go Noto KurrentSIL Open Font License 1.1obtained via BabelDOC

The OFL license text ships with each font family in app/fonts/.

↑↓ navigate openesc close