Node HTTP server that proxies the DeepL API and serves a single-page UI, plus per-domain glossary management for the four Scheer entities. Runs on Node's built-ins only; no dependencies. Containerized for Dokploy: the domain -> glossary_id map is written to DOMAINS_FILE (default /app/data/domains.json) so it can live on a mounted volume and survive redeploys. See DEPLOY.md. Co-Authored-By: Claude <noreply@anthropic.com>
Scheer Enterprise Translation Tool
A web tool connecting to the DeepL API (Growth plan) for text, document and PDF translation, plus token-optimized JSON translation, with per-domain glossaries for Scheer Group, Scheer IDS, Scheer PAS, and Scheer IMC.
Supported languages everywhere in the tool: German, German (Swiss), English (British), English (American), and French.
Why there's a small server involved
DeepL's API blocks direct calls from browser JavaScript (CORS) and your key
should never be embedded in public front-end code. server.js is a tiny
zero-dependency Node proxy: it serves the UI and relays requests to DeepL
using an API key configured only on the server. The key never touches
the browser, localStorage, or any client-side code.
Local setup (single user, on your own machine)
- Requires Node.js 18+ (uses built-in
fetch/FormData/Blob— nonpm installneeded). - Get your DeepL Growth API key from your DeepL account (developer/API settings).
- Copy
.env.exampleto.envin this folder and setDEEPL_API_KEY=.... (Alternatively, export it as a real environment variable:DEEPL_API_KEY=xxx node server.js.) - Run:
node server.js - Open http://localhost:3000. The usage indicator in the top-right corner of the header loads automatically (click the ↻ next to it to refresh anytime) and confirms the server is connected to DeepL.
Deploying for shared, multi-user access on a real domain
To make this available to everyone in the org at a URL like https://translate.yourcompany.com
instead of just localhost, you need three things: a server to run it on, a
domain pointed at that server, and a way to keep it running. None of this
requires code changes — server.js already listens on all network
interfaces, not just localhost.
- Get a server. Any internal VM, cloud instance, or on-prem box that can stay running and is reachable from wherever your users are (office network, VPN, or the public internet, depending on your needs).
- Point your domain at it. Create a DNS A/AAAA record for
translate.yourcompany.compointing at that server's IP. - Put a reverse proxy in front of it (e.g. Nginx or Caddy) to handle
HTTPS and forward requests to the Node app on port 3000. Caddy is the
simplest option — a
Caddyfilewith just:gets you automatic HTTPS via Let's Encrypt with no extra config.translate.yourcompany.com { reverse_proxy localhost:3000 } - Keep the Node process running with a process manager or container
instead of a terminal session — see the Docker option below, or use
something like
pm2or asystemdservice if you'd rather run it bare. - Configure
.envanddomains.jsononce, on that server — every user hitting the domain shares the same DeepL key and the same four domain glossaries. That's the intended design: one shared quota, one shared set of glossaries per Scheer entity, managed centrally.
Running it in Docker
docker build -t scheer-translation-tool .
# domains.json needs to exist before the bind mount below, otherwise Docker
# will create it as a directory instead of a file:
touch domains.json
docker run -d --name scheer-translation \
-p 3000:3000 \
--env-file .env \
-v "$(pwd)/domains.json:/app/domains.json" \
--restart unless-stopped \
scheer-translation-tool
Then point your reverse proxy at localhost:3000 on that host as described
above. --restart unless-stopped makes it survive reboots.
Important: this tool has no built-in login
Anyone who can reach the domain can translate documents (consuming your shared DeepL quota) and edit any domain's glossary — there's no user authentication built in. Depending on your needs:
- Restrict it to your internal network or VPN (don't expose it on the public internet), and/or
- Add HTTP Basic Auth at the reverse proxy layer (a couple of lines in Nginx or Caddy), and/or
- Put it behind whatever SSO/gateway your org already uses for internal tools.
Features
- Text Translation — free text in/out, placeholder protection (
{{var}},%s,{id}preserved verbatim), auto-detect or manual source language. - Document Translation — upload
.docx/.pptx/.xlsx/.pdf/.txt/.html, tracks DeepL's upload → poll → download flow, gives you a translated file to download. - PDF Translation — same upload → poll → download flow, dedicated to PDFs, with the full language selection.
- JSON Translation — paste or upload JSON, only extracts and translates user-facing string values (skips keys, numbers, booleans, nulls, URLs, HTML/code, template tags, and key-like identifiers such as
status_code_404), batches everything into as few DeepL requests as possible, and reconstructs the exact tree. Shows a stats panel (values scanned/sent/skipped, character savings). - Domain glossaries — a domain bar at the top of every page (always visible, independent of which tab you're on) lets you pick one of the four Scheer entities: Scheer Group, Scheer IDS, Scheer PAS, Scheer IMC. Each domain stores two fixed-direction glossaries: German → English and English → German, each with its own list of exact term pairs, created via DeepL's glossary API. Click ⚙ Domain settings to manage the currently selected domain's two term lists. Every translate action (text, document, PDF, JSON) automatically applies the matching direction's glossary and shows a note confirming it was applied.
How glossaries are stored (and why everyone sees the same ones)
Glossary terms are not stored in the browser (no localStorage, no per-user
data). Saving a term list calls DeepL's glossary API to create the actual
glossary on DeepL's servers, and the server writes the resulting glossary ID
plus a local copy of the terms to domains.json — a JSON file that lives
next to server.js on whichever machine is running the server. Every
request from every browser goes through that one server, which reads the
same domains.json, so anyone visiting the site sees and edits the same
shared German→English and English→German lists per domain. The settings
modal also re-fetches from the server each time it's opened, so you'll see
another teammate's most recent edits rather than a stale local copy.
For a small internal tool with a handful of admins editing glossaries
occasionally, a flat JSON file is a reasonable, low-maintenance choice —
there's no database to run or back up. If this ever needs to support many
people editing glossaries concurrently, or an audit trail of who changed
what, that would be the point to move domains.json into a real database.
Known limitations
- JSON skip rules are heuristic (regex-based). Always review the output panel before shipping translated JSON to production.
- Glossaries are recreated (not edited in place) on every save. DeepL's v2 glossaries are immutable, so saving a domain's German→English or English→German list deletes the old one and creates a new one with the full term list.
- Only these two directions are supported for glossaries. A glossary only applies to a translation when the request is going exactly German→English or English→German with an explicit (non-auto-detect) source language — the glossary API requires the source language to be set.
Files
server.js— proxy server (no external dependencies), readsDEEPL_API_KEYfrom.env/environmentindex.html— the single-page UI (styled with the Scheer brand palette: red, black, greys, whitespace)domains.json— auto-created on first run; stores each domain's two glossary IDs/metadata (see storage section above).env.example— copy to.envand fill in your keypackage.json— fornpm startconvenience (nothing to install)Dockerfile,.dockerignore— for containerized deployment (see above)