Problems registering, no confirmation email - see http://wiki.scribus.net/canvas/Forums_Registration
#!/usr/bin/env python
#
# Simple Python script to translate Sribus documents
# into various languages.
# Currently it supports the following languages:
# English, French, Italian, German, Spanish and Dutch
#
#
# Bastiaan Williams.
# bastiaanwilliams@protomail.com
import scribus
import urllib.request
import urllib.parse
import urllib.error
import json
# -----------------------------
# CONFIGURATION
# -----------------------------
DEEPL_API_KEY = "YOUR_OWN_API_KEY" # <-- fill in your own DeepL API key
DEEPL_URL = "https://api-free.deepl.com/v2/translate"
SUPPORTED_LANGS = {
"EN": "EN",
"NL": "NL",
"FR": "FR",
"DE": "DE",
"ES": "ES",
"IT": "IT"
}
LANG_HINT_WORDS = {
"EN": ["the", "and", "you", "with", "for", "to", "in"],
"NL": ["de", "het", "een", "en", "met", "niet", "dat"],
"FR": ["le", "la", "et", "les", "des", "une", "un"],
"DE": ["und", "die", "der", "das", "ist", "nicht"],
"ES": ["el", "la", "los", "las", "que", "para"],
"IT": ["il", "la", "che", "per", "non", "con"]
}
# Only translate frames that look like they are written in the source language
USE_LANGUAGE_FILTER = True
# -----------------------------
# HTTP / DEEPL HELPER
# -----------------------------
def deepl_request(params):
"""
Low level DeepL HTTP helper.
Returns JSON dict on success,
returns None on fatal error (401/403/429)
and shows message to the user.
"""
if not DEEPL_API_KEY or DEEPL_API_KEY == "YOUR_KEY_HERE":
scribus.messageBox(
"DeepL Error",
"Please set your DeepL API key in the script.",
scribus.ICON_WARNING
)
return None
params["auth_key"] = DEEPL_API_KEY
data = urllib.parse.urlencode(params).encode("utf-8")
try:
req = urllib.request.Request(DEEPL_URL, data=data)
f = urllib.request.urlopen(req)
js = json.loads(f.read().decode("utf-8"))
return js
except urllib.error.HTTPError as e:
code = e.code
if code == 429:
scribus.messageBox(
"DeepL limit reached",
"DeepL HTTP 429: Too many requests / quota used.\n"
"Translation will stop.",
scribus.ICON_WARNING
)
return None
if code in (401, 403):
scribus.messageBox(
"DeepL authentication / permission error",
(
"DeepL HTTP %d.\n"
"This usually means:\n"
"- Invalid API key\n"
"- Wrong endpoint (free vs pro)\n"
"- Or your account has no access.\n"
"Translation will stop."
) % code,
scribus.ICON_WARNING
)
return None
scribus.messageBox("DeepL HTTP Error", str(e), scribus.ICON_WARNING)
return None
except Exception as e:
scribus.messageBox("DeepL Error", str(e), scribus.ICON_WARNING)
return None
# -----------------------------
# API TEST
# -----------------------------
def test_deepl_connection():
"""
Sends a tiny test request to DeepL and reports if the API key works.
"""
params = {
"text": "Test",
"target_lang": "EN"
}
js = deepl_request(params)
if js is None:
return
try:
translated = js["translations"][0]["text"]
except Exception:
translated = "<could not read DeepL response>"
scribus.messageBox(
"DeepL Test",
"DeepL connection seems OK.\n"
"Sample translation result:\n\n%s" % translated,
scribus.ICON_INFORMATION
)
# -----------------------------
# TRANSLATION ENGINE
# -----------------------------
def translate_text(text, src, tgt):
"""Call DeepL for normal translation. Return None on fatal error."""
src_code = SUPPORTED_LANGS.get(src)
tgt_code = SUPPORTED_LANGS.get(tgt)
if not src_code or not tgt_code:
scribus.messageBox(
"Error",
"Invalid language code.",
scribus.ICON_WARNING
)
return text
params = {
"text": text,
"target_lang": tgt_code
}
if src_code != tgt_code:
params["source_lang"] = src_code
js = deepl_request(params)
if js is None:
return None
try:
return js["translations"][0]["text"]
except Exception as e:
scribus.messageBox("DeepL Parse Error", str(e), scribus.ICON_WARNING)
return text
# -----------------------------
# LANGUAGE DETECTION
# -----------------------------
def detect_lang_simple(text):
"""Very simple language guess based on a few common words."""
txt = text.lower()
for ch in ",.;:!?()[]{}'\"":
txt = txt.replace(ch, " ")
words = txt.split()
best = None
best_score = 0
for code, hints in LANG_HINT_WORDS.items():
score = 0
for w in words:
if w in hints:
score += 1
if score > best_score:
best = code
best_score = score
if best_score == 0:
return None
return best
# -----------------------------
# WORD + STYLE HELPERS
# -----------------------------
def get_words_with_positions(text):
"""
Split text into words by whitespace.
Return list of (start_index, length) per word.
"""
words = []
start = None
for i, ch in enumerate(text):
if ch.isspace():
if start is not None:
words.append((start, i - start))
start = None
else:
if start is None:
start = i
if start is not None:
words.append((start, len(text) - start))
return words
def detect_word_styles(frame_name, text, base_cstyle):
"""
For each word, detect if it has a uniform character style
different from the base style. Returns: word_index -> style_name.
"""
style_map = {}
try:
total_len = scribus.getTextLength(frame_name)
except Exception:
total_len = len(text)
char_styles = []
for i in range(total_len):
try:
scribus.selectText(i, 1, frame_name)
s = scribus.getCharacterStyle(frame_name)
except Exception:
s = base_cstyle
char_styles.append(s)
# reset selection
try:
scribus.selectText(0, 0, frame_name)
except Exception:
pass
word_positions = get_words_with_positions(text)
for idx, (start, length) in enumerate(word_positions):
if start + length > len(char_styles):
continue
styles_in_word = char_styles[start:start + length]
if not styles_in_word:
continue
first_style = styles_in_word[0]
if all(s == first_style for s in styles_in_word):
if first_style and first_style != base_cstyle:
style_map[idx] = first_style
return style_map
def apply_word_styles(frame_name, text, word_style_map):
"""
Apply character styles from word_style_map (word_index -> style_name)
to corresponding words in the given text.
"""
word_positions = get_words_with_positions(text)
for idx, (start, length) in enumerate(word_positions):
if idx not in word_style_map:
continue
style_name = word_style_map[idx]
try:
scribus.selectText(start, length, frame_name)
scribus.setCharacterStyle(style_name, frame_name)
except Exception:
continue
# clear selection
try:
scribus.selectText(0, 0, frame_name)
except Exception:
pass
# -----------------------------
# FRAME PROCESSING
# -----------------------------
def process_frames(frames, src, tgt):
"""
Translate given frames, preserving:
- font
- font size
- text color
- alignment
- line spacing (leading)
- paragraph style
- base character style
- word-level character styles (e.g. bold words)
"""
translated = 0
skipped = 0
src_norm = src # ES stays ES
for frame_name in frames:
text = scribus.getText(frame_name)
if not text.strip():
continue
# --- language filter ---
if USE_LANGUAGE_FILTER:
detected = detect_lang_simple(text)
if detected and detected != src_norm:
skipped += 1
continue
# --- remember base styles / font / color / line spacing / alignment ---
try:
base_font = scribus.getFont(frame_name)
except Exception:
base_font = None
try:
base_size = scribus.getFontSize(frame_name)
except Exception:
base_size = None
try:
base_color = scribus.getTextColor(frame_name)
except Exception:
base_color = None
try:
base_linespacing = scribus.getLineSpacing(frame_name)
except Exception:
base_linespacing = None
try:
base_align = scribus.getAlignment(frame_name)
except Exception:
base_align = None
try:
base_pstyle = scribus.getParagraphStyle(frame_name)
except Exception:
base_pstyle = None
try:
base_cstyle = scribus.getCharacterStyle(frame_name)
except Exception:
base_cstyle = ""
if base_cstyle is None:
base_cstyle = ""
# --- detect word-level styles in source text ---
word_style_map = detect_word_styles(frame_name, text, base_cstyle)
# --- translate ---
new_text = translate_text(text, src, tgt)
if new_text is None:
# fatal DeepL error, stop
break
scribus.setText(new_text, frame_name)
# --- re-apply base font / size / color / line spacing / alignment / styles ---
try:
total_len = scribus.getTextLength(frame_name)
except Exception:
total_len = len(new_text)
if total_len > 0:
try:
scribus.selectText(0, total_len, frame_name)
except Exception:
pass
if base_font:
try:
scribus.setFont(base_font, frame_name)
except Exception:
pass
if base_size is not None:
try:
scribus.setFontSize(base_size, frame_name)
except Exception:
pass
if base_color:
try:
scribus.setTextColor(base_color, frame_name)
except Exception:
pass
if base_linespacing is not None:
try:
scribus.setLineSpacing(base_linespacing, frame_name)
except Exception:
pass
if base_align is not None:
try:
scribus.setAlignment(base_align, frame_name)
except Exception:
pass
if base_pstyle:
try:
scribus.setParagraphStyle(base_pstyle, frame_name)
except Exception:
pass
if base_cstyle:
try:
scribus.setCharacterStyle(base_cstyle, frame_name)
except Exception:
pass
# clear selection
try:
scribus.selectText(0, 0, frame_name)
except Exception:
pass
# --- apply word-level styles on translated text ---
apply_word_styles(frame_name, new_text, word_style_map)
translated += 1
msg = (
"Translation finished.\n\n"
"Translated frames: %d\n"
"Skipped (different language): %d"
) % (translated, skipped)
scribus.messageBox("Done", msg, scribus.ICON_NONE)
# -----------------------------
# MENUS (NUMBER-BASED)
# -----------------------------
# Note: The Scribus Python interpreter does not support
# dropdown menus or custom GUI buttons.
# Only basic dialogs like messageBox() and valueDialog() are available.
def choose_action():
"""
Step 0 – What do you want to do?
[1] Test DeepL
[2] Translate
[3] Cancel
"""
text = (
"What do you want to do?\n\n"
"[1] Test DeepL connection (small test request)\n"
"[2] Translate text frames\n"
"[3] Cancel\n\n"
"Type 1, 2 or 3:"
)
choice = scribus.valueDialog(
"Action Menu",
text,
"2"
)
if not choice:
return None
choice = choice.strip()
if choice == "1":
return "test"
if choice == "2":
return "translate"
return None
def choose_mode():
"""
Step 1 – What to translate?
[1] Whole current page
[2] Selected text frames
[3] Cancel
"""
text = (
"Step 1/3 – What do you want to translate?\n\n"
"[1] Whole current page (all text frames on this page)\n"
"[2] Selected text frames only\n"
"[3] Cancel\n\n"
"Type 1, 2 or 3:"
)
choice = scribus.valueDialog(
"Translation Mode",
text,
"1"
)
if not choice:
return None
choice = choice.strip()
if choice == "1":
return "page"
if choice == "2":
return "selected"
return None
def choose_language(title, default_code="EN"):
text = (
"Choose language code:\n\n"
"EN = English\n"
"NL = Dutch\n"
"FR = French\n"
"DE = German\n"
"ES = Spanish\n"
"IT = Italian\n"
)
lang = scribus.valueDialog(
title,
text,
default_code
)
if not lang:
return None
lang = lang.strip().upper()
if lang in SUPPORTED_LANGS:
return lang
scribus.messageBox(
"Error",
"Invalid language code. Use EN, NL, FR, DE, ES, or IT.",
scribus.ICON_WARNING
)
return None
# -----------------------------
# MAIN
# -----------------------------
def main():
if not scribus.haveDoc():
scribus.messageBox(
"Error",
"Open a document first.",
scribus.ICON_WARNING
)
return
action = choose_action()
if action is None:
return
if action == "test":
test_deepl_connection()
return
# action == "translate"
mode = choose_mode()
if not mode:
return
src = choose_language("Step 2/3 – Source language", "EN")
if not src:
return
tgt = choose_language("Step 3/3 – Target language", "IT")
if not tgt:
return
if mode == "page":
frames = [
item[0] for item in scribus.getPageItems()
if item[1] == 4
]
if not frames:
scribus.messageBox(
"Info",
"No text frames found on this page.",
scribus.ICON_INFORMATION
)
return
process_frames(frames, src, tgt)
elif mode == "selected":
if scribus.selectionCount() == 0:
scribus.messageBox(
"Error",
"No frames selected.",
scribus.ICON_WARNING
)
return
frames = []
for i in range(scribus.selectionCount()):
name = scribus.getSelectedObject(i)
if scribus.getObjectType(name) == "TextFrame":
frames.append(name)
if not frames:
scribus.messageBox(
"Error",
"No selected text frames.",
scribus.ICON_WARNING
)
return
process_frames(frames, src, tgt)
if __name__ == "__main__":
main()
Quote from: forest_bear59 on December 18, 2022, 08:01:13 PMI have evaluated the MonthlyCalendar script and find it extremely useful.