#!/usr/bin/env python3
"""Interaktiver Transifex-Push: fragt, welche Dateien hochgeladen werden.
Das Skript liest .tx/config des aktuellen Branches, ordnet jeder .po-Datei
ihren Transifex-Resource-Namen zu und stellt sie zur Auswahl. Gepusht wird
mit der forcierten Kombination, die wir beim Abgleich benutzen:
tx push -t -l de -f --skip --replace-edited-strings ...
-t nur Uebersetzungen (die Quelltexte kommen
aus CPython, nicht von uns)
-l de nur die deutsche Sprache
-f Zeitstempel-Sperre aus ("remote is newer")
--replace-edited-strings ueberschreibt auch in Transifex bearbeitete
Strings -- GitHub ist die Quelle der Wahrheit
--skip bricht nicht ab, wenn eine Ressource zickt
Aufruf:
python tools/tx_push.py # geaenderte Dateien vorschlagen
python tools/tx_push.py --alle # alle Dateien aus .tx/config
python tools/tx_push.py howto # nach Pfad filtern
python tools/tx_push.py library/csv.po
python tools/tx_push.py --dry-run howto # nur zeigen, nicht pushen
"""
import argparse
import configparser
import re
import subprocess
import sys
from pathlib import Path
try:
import polib
except ImportError:
polib = None # Fortschrittsanzeige ist dann eben leer
def git(*args: str) -> str:
e = subprocess.run(["git", *args], capture_output=True, text=True)
return e.stdout if e.returncode == 0 else ""
def repo_wurzel() -> Path:
aus = git("rev-parse", "--show-toplevel").strip()
if not aus:
sys.exit("Kein Git-Repository -- bitte im python-docs-de-Verzeichnis starten.")
return Path(aus)
def ressourcen(wurzel: Path) -> dict:
"""{po-Pfad: (projekt, resource)} aus .tx/config."""
pfad = wurzel / ".tx" / "config"
if not pfad.is_file():
sys.exit(f"{pfad} nicht gefunden.")
cfg = configparser.ConfigParser()
cfg.read(pfad, encoding="utf-8")
aus = {}
for name in cfg.sections():
# [o:python-doc:p:python-314:r:howto--index]
m = re.match(r"o:([^:]+):p:([^:]+):r:(.+)", name)
if not m:
continue
_org, projekt, resource = m.groups()
abschnitt = cfg[name]
datei = abschnitt.get("trans.de") or abschnitt.get("file_filter", "")
datei = datei.replace("", "de").strip()
if datei:
aus[datei] = (projekt, resource)
if not aus:
sys.exit("In .tx/config wurden keine Ressourcen gefunden.")
return aus
def geaenderte_dateien(wurzel: Path) -> set:
""".po-Dateien, die sich gegenueber origin/ oder im Baum geaendert haben."""
branch = git("branch", "--show-current").strip()
kandidaten = set()
for befehl in (
["diff", "--name-only"], # unversionierte Aenderungen
["diff", "--name-only", "--cached"], # gestaged
["diff", "--name-only", f"origin/{branch}...HEAD"], # noch nicht gepusht
):
for zeile in git(*befehl).splitlines():
if zeile.endswith(".po"):
kandidaten.add(zeile)
return kandidaten
def stand(wurzel: Path, datei: str) -> str:
"""'123/456 (27 %)' -- oder leer, wenn polib fehlt."""
if polib is None:
return ""
p = wurzel / datei
if not p.is_file():
return "fehlt lokal"
try:
po = polib.pofile(str(p))
except Exception:
return "nicht lesbar"
gesamt = len([e for e in po if not e.obsolete])
fertig = len([e for e in po if not e.obsolete and e.translated()])
prozent = 100 * fertig // gesamt if gesamt else 0
return f"{fertig}/{gesamt} ({prozent} %)"
def auswahl_lesen(anzahl: int) -> list:
"""'1,3-5' oder 'a' einlesen und in Indizes uebersetzen."""
while True:
roh = input("\nNummern (z. B. 1,3-5), a = alle, q = Abbruch: ").strip().lower()
if roh in ("q", ""):
sys.exit("Abgebrochen.")
if roh == "a":
return list(range(anzahl))
indizes = set()
try:
for teil in roh.replace(" ", "").split(","):
if "-" in teil:
von, bis = teil.split("-", 1)
indizes.update(range(int(von) - 1, int(bis)))
else:
indizes.add(int(teil) - 1)
except ValueError:
print("Das habe ich nicht verstanden.")
continue
if any(i < 0 or i >= anzahl for i in indizes):
print(f"Nur 1 bis {anzahl} sind gueltig.")
continue
return sorted(indizes)
def main() -> None:
p = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
)
p.add_argument("muster", nargs="*", help="Pfadteile zum Filtern, z. B. howto")
p.add_argument("--alle", action="store_true",
help="alle Dateien aus .tx/config zur Auswahl stellen")
p.add_argument("--dry-run", action="store_true",
help="Befehl nur anzeigen, nicht ausfuehren")
args = p.parse_args()
wurzel = repo_wurzel()
branch = git("branch", "--show-current").strip()
karte = ressourcen(wurzel)
projekt = next(iter(karte.values()))[0]
dateien = sorted(karte)
herkunft = "alle Ressourcen aus .tx/config"
if args.muster:
dateien = [d for d in dateien if any(m in d for m in args.muster)]
herkunft = "Treffer fuer " + ", ".join(args.muster)
elif not args.alle:
geaendert = geaenderte_dateien(wurzel) & set(karte)
if geaendert:
dateien = sorted(geaendert)
herkunft = "geaenderte Dateien"
else:
print("Keine geaenderten .po-Dateien gefunden -- zeige alle.\n")
if not dateien:
sys.exit("Keine passende Datei gefunden.")
print(f"Branch {branch} -> Transifex-Projekt {projekt} ({herkunft})\n")
breite = max(len(d) for d in dateien)
for i, d in enumerate(dateien, 1):
print(f" {i:>3} {d: