Translating Word-files to any language-using DeepL API

andreasPython Code2 weeks ago119 Views

Here’s a ready-to-run Python GUI app (Tkinter) that translates Word documents (.docx) using the DeepL API (Document Translation). It uploads your file to DeepL, polls the job status, and downloads the translated document while preserving formatting.

It also supports DeepL Free (api-free.deepl.com) and DeepL Pro (api.deepl.com).
Just paste your API key, pick a target language, choose a .docx, and click Translate.


Features

  • Translates .docx using DeepL’s document endpoint (best formatting preservation)
  • Auto-detects Free vs Pro endpoint (or choose manually)
  • Choose source language (optional) or let DeepL auto-detect
  • Progress bar + live status logs
  • Saves as YourFile.translated.<LANG>.docx in chosen output folder

Requirements

pip install requests python-dotenv

Optional: Create a .env file with
DEEPL_API_KEY=your_key_here
The app will auto-load it.


How to get a DeepL API key

  • DeepL Free (100k chars/month): https://www.deepl.com/pro-api (Free plan)
  • DeepL Pro: higher limits / features
  • Keys ending with :fx are usually Free keys.

Supported Target Languages (DeepL)

BG, CS, DA, DE, EL, EN, EN-GB, EN-US, ES, ET, FI, FR, HU, ID, IT, JA, KO, LT, LV, NB, NL, PL, PT, PT-BR, PT-PT, RO, RU, SK, SL, SV, TR, UK, ZH


Full Script (save as deepl_docx_translator.py)

#!/usr/bin/env python3
"""
DeepL Document Translator GUI (for .docx)
-----------------------------------------
- Translates Word documents using DeepL's document translation API
- Preserves formatting (DeepL handles conversion)
- Works with DeepL Free and Pro

Setup:
  pip install requests python-dotenv
  (optional) .env file with: DEEPL_API_KEY=your_key

Run:
  python deepl_docx_translator.py
"""

import os
import sys
import time
import threading
import traceback
import requests
from tkinter import Tk, StringVar, BooleanVar, filedialog, messagebox
from tkinter import ttk
from dotenv import load_dotenv
from pathlib import Path

# ---------- Config ----------
APP_TITLE = "DeepL Document Translator (DOCX)"
DEFAULT_POLL_SECONDS = 2
TIMEOUT_SECONDS = 60 * 10  # 10 minutes safety (adjust as you like)

# DeepL endpoints
DEEPL_PRO = "https://api.deepl.com"
DEEPL_FREE = "https://api-free.deepl.com"

# Known target languages & nice labels (DeepL supports others; you can fetch dynamically if desired)
TARGET_LANGS = [
    ("Auto-detect source → English (EN)", "EN"),
    ("English (US) (EN-US)", "EN-US"),
    ("English (UK) (EN-GB)", "EN-GB"),
    ("German (DE)", "DE"),
    ("French (FR)", "FR"),
    ("Spanish (ES)", "ES"),
    ("Italian (IT)", "IT"),
    ("Dutch (NL)", "NL"),
    ("Portuguese (PT)", "PT"),
    ("Portuguese (Brazil) (PT-BR)", "PT-BR"),
    ("Portuguese (Portugal) (PT-PT)", "PT-PT"),
    ("Polish (PL)", "PL"),
    ("Czech (CS)", "CS"),
    ("Danish (DA)", "DA"),
    ("Greek (EL)", "EL"),
    ("Estonian (ET)", "ET"),
    ("Finnish (FI)", "FI"),
    ("Hungarian (HU)", "HU"),
    ("Indonesian (ID)", "ID"),
    ("Japanese (JA)", "JA"),
    ("Korean (KO)", "KO"),
    ("Lithuanian (LT)", "LT"),
    ("Latvian (LV)", "LV"),
    ("Norwegian Bokmål (NB)", "NB"),
    ("Romanian (RO)", "RO"),
    ("Russian (RU)", "RU"),
    ("Slovak (SK)", "SK"),
    ("Slovenian (SL)", "SL"),
    ("Swedish (SV)", "SV"),
    ("Turkish (TR)", "TR"),
    ("Ukrainian (UK)", "UK"),
    ("Bulgarian (BG)", "BG"),
    ("Chinese (ZH)", "ZH"),
]

# Optional: Source language options (leave empty for auto-detect)
SOURCE_LANGS = [
    ("Auto-detect (recommended)", ""),
    ("English (EN)", "EN"),
    ("German (DE)", "DE"),
    ("French (FR)", "FR"),
    ("Spanish (ES)", "ES"),
    ("Italian (IT)", "IT"),
    ("Dutch (NL)", "NL"),
    ("Polish (PL)", "PL"),
    ("Portuguese (PT)", "PT"),
    ("Portuguese (Brazil) (PT-BR)", "PT-BR"),
    ("Portuguese (Portugal) (PT-PT)", "PT-PT"),
    ("Czech (CS)", "CS"),
    ("Danish (DA)", "DA"),
    ("Greek (EL)", "EL"),
    ("Estonian (ET)", "ET"),
    ("Finnish (FI)", "FI"),
    ("Hungarian (HU)", "HU"),
    ("Indonesian (ID)", "ID"),
    ("Japanese (JA)", "JA"),
    ("Korean (KO)", "KO"),
    ("Lithuanian (LT)", "LT"),
    ("Latvian (LV)", "LV"),
    ("Norwegian Bokmål (NB)", "NB"),
    ("Romanian (RO)", "RO"),
    ("Russian (RU)", "RU"),
    ("Slovak (SK)", "SK"),
    ("Slovenian (SL)", "SL"),
    ("Swedish (SV)", "SV"),
    ("Turkish (TR)", "TR"),
    ("Ukrainian (UK)", "UK"),
    ("Bulgarian (BG)", "BG"),
    ("Chinese (ZH)", "ZH"),
]

# ---------- Helpers ----------
def infer_base_url(api_key: str, force_free: bool) -> str:
    """
    Decide which DeepL host to use.
    - If user checked "Use Free API" → api-free
    - Else if key ends with ':fx' (common for free keys) → api-free
    - Otherwise → api (Pro)
    """
    if force_free:
        return DEEPL_FREE
    if api_key and api_key.endswith(":fx"):
        return DEEPL_FREE
    return DEEPL_PRO

def translate_doc(api_key: str, base_url: str, src_file: Path, out_dir: Path, target_lang: str, source_lang: str, formality: str = None):
    """
    Upload a document to DeepL, poll status, download the translated file.
    Returns the output file path.
    """
    # 1) Upload
    url_upload = f"{base_url}/v2/document"
    files = {"file": (src_file.name, open(src_file, "rb"), "application/vnd.openxmlformats-officedocument.wordprocessingml.document")}
    data = {"target_lang": target_lang}
    if source_lang:
        data["source_lang"] = source_lang
    if formality:  # e.g., "more", "less", "prefer_more", "prefer_less"
        data["formality"] = formality

    headers = {"Authorization": f"DeepL-Auth-Key {api_key}"}

    resp = requests.post(url_upload, files=files, data=data, headers=headers, timeout=60)
    if resp.status_code != 200:
        raise RuntimeError(f"Upload failed: {resp.status_code} {resp.text}")
    payload = resp.json()
    document_id = payload["document_id"]
    document_key = payload["document_key"]

    # 2) Poll
    url_status = f"{base_url}/v2/document/{document_id}"
    start_time = time.time()
    while True:
        resp = requests.post(url_status, data={"document_key": document_key}, headers=headers, timeout=60)
        if resp.status_code != 200:
            raise RuntimeError(f"Status failed: {resp.status_code} {resp.text}")
        st = resp.json()
        status = st.get("status", "")
        secs = int(time.time() - start_time)
        yield ("status", f"{status} ({secs}s)")

        if status == "done":
            break
        if status in ("error", "failed"):
            message = st.get("message", "Unknown error")
            raise RuntimeError(f"Translation failed: {message}")
        if time.time() - start_time > TIMEOUT_SECONDS:
            raise TimeoutError("Timed out waiting for DeepL to finish.")
        time.sleep(DEFAULT_POLL_SECONDS)

    # 3) Download result
    url_result = f"{base_url}/v2/document/{document_id}/result"
    resp = requests.post(url_result, data={"document_key": document_key}, headers=headers, timeout=120)
    if resp.status_code != 200:
        raise RuntimeError(f"Download failed: {resp.status_code} {resp.text}")

    out_dir.mkdir(parents=True, exist_ok=True)
    out_name = f"{src_file.stem}.translated.{target_lang}.docx"
    out_path = out_dir / out_name
    with open(out_path, "wb") as f:
        f.write(resp.content)

    yield ("done", str(out_path))

# ---------- GUI ----------
class App:
    def __init__(self, root: Tk):
        self.root = root
        self.root.title(APP_TITLE)
        self.root.geometry("720x520")
        self.root.minsize(680, 500)

        # variables
        load_dotenv()  # load .env if present
        self.api_key = StringVar(value=os.getenv("DEEPL_API_KEY", ""))
        self.use_free = BooleanVar(value=False)  # force api-free
        self.source_lang = StringVar(value=SOURCE_LANGS[0][1])  # ""
        self.target_lang = StringVar(value="EN")
        self.input_path = StringVar(value="")
        self.output_dir = StringVar(value=str(Path.home() / "deepl_translations"))
        self.formality = StringVar(value="")  # "", "more", "less", "prefer_more", "prefer_less"

        # layout
        pad = {"padx": 10, "pady": 8}

        frm = ttk.Frame(root)
        frm.pack(fill="both", expand=True)

        row = 0
        ttk.Label(frm, text="DeepL API Key:").grid(row=row, column=0, sticky="e", **pad)
        self.api_entry = ttk.Entry(frm, textvariable=self.api_key, show="", width=50)
        self.api_entry.grid(row=row, column=1, sticky="we", **pad, columnspan=2)
        self.show_key = BooleanVar(value=False)
        chk = ttk.Checkbutton(frm, text="Show", variable=self.show_key, command=self._toggle_key)
        chk.grid(row=row, column=3, sticky="w", **pad)

        row += 1
        ttk.Checkbutton(frm, text="Use Free API (api-free.deepl.com)", variable=self.use_free).grid(row=row, column=1, sticky="w", **pad, columnspan=2)

        row += 1
        ttk.Label(frm, text="Source language:").grid(row=row, column=0, sticky="e", **pad)
        self.src_combo = ttk.Combobox(frm, state="readonly", values=[name for name, code in SOURCE_LANGS])
        self.src_combo.grid(row=row, column=1, sticky="we", **pad)
        self.src_combo.current(0)
        self.src_combo.bind("<<ComboboxSelected>>", self._on_source_select)

        ttk.Label(frm, text="Target language:").grid(row=row, column=2, sticky="e", **pad)
        self.tgt_combo = ttk.Combobox(frm, state="readonly", values=[name for name, code in TARGET_LANGS])
        self.tgt_combo.grid(row=row, column=3, sticky="we", **pad)
        # default to EN
        for i,(name, code) in enumerate(TARGET_LANGS):
            if code == "EN":
                self.tgt_combo.current(i)
                break
        self.tgt_combo.bind("<<ComboboxSelected>>", self._on_target_select)

        row += 1
        ttk.Label(frm, text="Formality (optional):").grid(row=row, column=0, sticky="e", **pad)
        self.form_combo = ttk.Combobox(frm, state="readonly",
                                       values=["", "more", "less", "prefer_more", "prefer_less"])
        self.form_combo.grid(row=row, column=1, sticky="we", **pad)
        self.form_combo.current(0)
        self.form_combo.bind("<<ComboboxSelected>>", self._on_formality_select)

        row += 1
        ttk.Label(frm, text="Input .docx:").grid(row=row, column=0, sticky="e", **pad)
        self.in_entry = ttk.Entry(frm, textvariable=self.input_path, width=50)
        self.in_entry.grid(row=row, column=1, sticky="we", **pad)
        ttk.Button(frm, text="Browse…", command=self._choose_input).grid(row=row, column=2, sticky="w", **pad)

        row += 1
        ttk.Label(frm, text="Output folder:").grid(row=row, column=0, sticky="e", **pad)
        self.out_entry = ttk.Entry(frm, textvariable=self.output_dir, width=50)
        self.out_entry.grid(row=row, column=1, sticky="we", **pad)
        ttk.Button(frm, text="Select…", command=self._choose_output).grid(row=row, column=2, sticky="w", **pad)

        row += 1
        self.btn_translate = ttk.Button(frm, text="Translate", command=self._start_translation)
        self.btn_translate.grid(row=row, column=1, sticky="we", **pad)
        ttk.Button(frm, text="Quit", command=root.destroy).grid(row=row, column=2, sticky="we", **pad)

        # progress + log
        row += 1
        self.pbar = ttk.Progressbar(frm, mode="indeterminate")
        self.pbar.grid(row=row, column=0, columnspan=4, sticky="we", **pad)

        row += 1
        ttk.Label(frm, text="Status:").grid(row=row, column=0, sticky="ne", **pad)
        self.log = ttk.Treeview(frm, columns=("msg",), show="tree", height=10)
        self.log.grid(row=row, column=1, columnspan=3, sticky="nsew", **pad)

        # stretch
        frm.columnconfigure(1, weight=1)
        frm.columnconfigure(3, weight=1)
        frm.rowconfigure(row, weight=1)

    # --- GUI handlers ---
    def _toggle_key(self):
        self.api_entry.config(show="" if self.show_key.get() else "")

    def _on_source_select(self, _evt=None):
        idx = self.src_combo.current()
        self.source_lang.set(SOURCE_LANGS[idx][1])

    def _on_target_select(self, _evt=None):
        idx = self.tgt_combo.current()
        self.target_lang.set(TARGET_LANGS[idx][1])

    def _on_formality_select(self, _evt=None):
        self.formality.set(self.form_combo.get().strip() or "")

    def _choose_input(self):
        path = filedialog.askopenfilename(
            title="Choose a DOCX file",
            filetypes=[("Word Document", "*.docx")],
        )
        if path:
            self.input_path.set(path)

    def _choose_output(self):
        path = filedialog.askdirectory(title="Choose output folder")
        if path:
            self.output_dir.set(path)

    def _log(self, text: str):
        self.log.insert("", "end", text=text)
        self.log.see(self.log.get_children()[-1])

    def _start_translation(self):
        # validate
        api_key = self.api_key.get().strip()
        if not api_key:
            messagebox.showerror("Missing API key", "Please paste your DeepL API key.")
            return

        src = Path(self.input_path.get().strip())
        if not src.exists() or src.suffix.lower() != ".docx":
            messagebox.showerror("Invalid file", "Please choose a valid .docx file.")
            return

        out_dir = Path(self.output_dir.get().strip()) if self.output_dir.get().strip() else Path.cwd()
        target_lang = self.target_lang.get().strip()
        source_lang = self.source_lang.get().strip()
        formality = self.formality.get().strip() or None

        base_url = infer_base_url(api_key, self.use_free.get())

        # disable UI, start progress
        self.btn_translate.config(state="disabled")
        self.pbar.start(10)
        self._log(f"Starting… Target={target_lang}, Source={'auto' if not source_lang else source_lang}, Endpoint={base_url}")

        def worker():
            try:
                for kind, msg in translate_doc(api_key, base_url, src, out_dir, target_lang, source_lang, formality):
                    if kind == "status":
                        self.root.after(0, lambda m=msg: self._log(f"Status: {m}"))
                    elif kind == "done":
                        self.root.after(0, lambda m=msg: self._log(f"Done → {m}"))
                self.root.after(0, lambda: messagebox.showinfo("Success", "Translation complete!"))
            except Exception as e:
                err = f"{type(e).__name__}: {e}"
                tb = traceback.format_exc()
                self.root.after(0, lambda: self._log(err))
                self.root.after(0, lambda: self._log(tb))
                self.root.after(0, lambda: messagebox.showerror("Error", err))
            finally:
                self.root.after(0, lambda: self.btn_translate.config(state="normal"))
                self.root.after(0, lambda: self.pbar.stop())

        threading.Thread(target=worker, daemon=True).start()

def main():
    root = Tk()
    style = ttk.Style()
    try:
        style.theme_use("clam")
    except:
        pass
    App(root)
    root.mainloop()

if __name__ == "__main__":
    main()

How it works (step-by-step)

  1. Upload your .docx to DeepL via POST /v2/document, passing target_lang (and optional source_lang / formality).
  2. DeepL returns a document_id and document_key.
  3. The app polls POST /v2/document/{document_id} every 2 seconds until status is done.
  4. It then downloads the result via POST /v2/document/{document_id}/result.
  5. The translated file is saved to your chosen folder as YourFile.translated.<LANG>.docx.

Notes & Tips

  • Free vs Pro: If your key ends with :fx, it’s likely a Free key → the app automatically selects api-free.deepl.com. You can also tick “Use Free API” manually.
  • Formality: Some languages support more/less/prefer_more/prefer_less. Leave blank if unsure.
  • Other formats: DeepL’s document API supports additional formats (like .pptx, .xlsx). You can add them by adjusting the file dialog and MIME type.
  • Rate limits & size: Your DeepL plan limits size/characters. Errors will appear in the status log.

1 Votes: 1 Upvotes, 0 Downvotes (1 Points)

Leave a reply

Previous Post

Next Post

Loading Next Post...
Loading

Signing-in 3 seconds...