Skip to content

Commit 8927490

Browse files
committed
enxtensions and improvements
1 parent 087781b commit 8927490

7 files changed

Lines changed: 1638 additions & 0 deletions

File tree

‎.github/workflows/sync-check.yml‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
name: Sync Check with Upstream
2+
3+
on:
4+
schedule:
5+
- cron: '0 3 * * 1' # jeden Montag um 3 Uhr nachts
6+
workflow_dispatch:
7+
8+
jobs:
9+
check-sync:
10+
runs-on: ubuntu-latest
11+
strategy:
12+
fail-fast: false
13+
matrix:
14+
version: ['3.14', '3.15']
15+
steps:
16+
- name: Translation-Repo auschecken
17+
uses: actions/checkout@v4
18+
with:
19+
ref: ${{ matrix.version }}
20+
21+
- name: Passenden CPython-Branch auschecken
22+
uses: actions/checkout@v4
23+
with:
24+
repository: python/cpython
25+
ref: ${{ matrix.version }}
26+
path: cpython-src
27+
28+
- name: gettext installieren (für msgmerge)
29+
run: sudo apt-get update && sudo apt-get install -y gettext
30+
31+
- name: Python + Sphinx einrichten
32+
run: |
33+
python3 -m venv cpython-src/Doc/venv
34+
cpython-src/Doc/venv/bin/pip install sphinx blurb
35+
36+
- name: Frische POT-Dateien bauen
37+
run: |
38+
cd cpython-src/Doc
39+
PATH="./venv/bin:$PATH" make gettext
40+
41+
- name: powrap installieren (für einheitliche Zeilenumbrüche)
42+
run: pip install --break-system-packages powrap
43+
44+
- name: msgmerge gegen alle .po-Dateien laufen lassen
45+
run: |
46+
for pot in $(find cpython-src/Doc/build/gettext -name "*.pot"); do
47+
rel="${pot#cpython-src/Doc/build/gettext/}"
48+
po="${rel%.pot}.po"
49+
if [ -f "$po" ]; then
50+
msgmerge --quiet --update --backup=none "$po" "$pot"
51+
else
52+
mkdir -p "$(dirname "$po")"
53+
msginit --no-translator --locale=de --input="$pot" --output-file="$po"
54+
fi
55+
powrap "$po"
56+
done
57+
58+
- name: Prüfen, ob sich etwas geändert hat
59+
id: check
60+
run: |
61+
if [ -z "$(git status --porcelain)" ]; then
62+
echo "changed=false" >> "$GITHUB_OUTPUT"
63+
else
64+
echo "changed=true" >> "$GITHUB_OUTPUT"
65+
fi
66+
67+
- name: PR mit den Aktualisierungen erstellen
68+
if: steps.check.outputs.changed == 'true'
69+
uses: peter-evans/create-pull-request@v6
70+
with:
71+
branch: sync-upstream-${{ matrix.version }}
72+
base: ${{ matrix.version }}
73+
title: "Sync mit Upstream-Änderungen (${{ matrix.version }})"
74+
body: |
75+
Automatisch erkannte Änderungen im englischen Original-Quelltext.
76+
Neue `fuzzy`-Markierungen und fehlende Strings bitte manuell prüfen.
77+
commit-message: "Sync mit Upstream-Änderungen (${{ matrix.version }})"

‎tools/branch_diff.py‎

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
#!/usr/bin/env python3
2+
"""Vergleicht die Uebersetzungen zweier Branches Datei fuer Datei.
3+
4+
Fuer jede .po-Datei wird verglichen, wie viele Eintraege in beiden Branches
5+
denselben msgstr haben. Der Prozentwert bezieht sich auf die Anzahl der
6+
Eintraege im *neueren* Branch (Vorgabe 3.15), weil der die Zielversion ist:
7+
8+
Aehnlichkeit = identische Eintraege / Eintraege in 3.15
9+
10+
Ein Eintrag gilt als identisch, wenn msgid (samt msgctxt) und msgstr in
11+
beiden Branches exakt uebereinstimmen -- auch dann, wenn beide leer sind.
12+
Obsolete Eintraege (#~) und der Kopfblock bleiben aussen vor.
13+
14+
Aufruf:
15+
python branch_diff.py # 3.14 gegen 3.15
16+
python branch_diff.py 3.14 main
17+
python branch_diff.py --csv bericht.csv
18+
python branch_diff.py --nur-abweichungen
19+
20+
Es wird nichts ausgecheckt und nichts geaendert -- die Inhalte kommen
21+
ueber "git show" direkt aus den Branches.
22+
"""
23+
24+
import argparse
25+
import csv
26+
import os
27+
import subprocess
28+
import sys
29+
import tempfile
30+
31+
try:
32+
import polib
33+
except ImportError:
34+
sys.exit("polib fehlt. Im aktiven venv installieren: pip install polib")
35+
36+
AUSSCHLUSS = (".venv/", ".git/")
37+
38+
KLASSEN = [
39+
(100.0, "deckungsgleich"),
40+
(80.0, "ueber 80 %"),
41+
(50.0, "ueber 50 %"),
42+
(30.0, "ueber 30 %"),
43+
(0.0, "unter 30 %"),
44+
]
45+
46+
47+
def git(*args: str) -> str:
48+
"""git aufrufen und stdout zurueckgeben."""
49+
return subprocess.run(
50+
["git", *args], capture_output=True, text=True, check=True
51+
).stdout
52+
53+
54+
def po_dateien(branch: str) -> dict:
55+
"""Alle .po-Dateien eines Branches als {pfad: blob-hash}."""
56+
aus = {}
57+
for zeile in git("ls-tree", "-r", branch).splitlines():
58+
kopf, pfad = zeile.split("\t", 1)
59+
_modus, art, blob = kopf.split()
60+
if art != "blob" or not pfad.endswith(".po"):
61+
continue
62+
if pfad.startswith(AUSSCHLUSS):
63+
continue
64+
aus[pfad] = blob
65+
return aus
66+
67+
68+
def eintraege(blob: str) -> dict:
69+
"""Blob-Inhalt parsen: {(msgctxt, msgid): msgstr}."""
70+
inhalt = git("cat-file", "blob", blob)
71+
# polib will einen Dateipfad -- also kurz zwischenspeichern.
72+
with tempfile.NamedTemporaryFile(
73+
"w", suffix=".po", encoding="utf-8", delete=False
74+
) as f:
75+
f.write(inhalt)
76+
tmp = f.name
77+
try:
78+
po = polib.pofile(tmp)
79+
return {(e.msgctxt, e.msgid): e.msgstr for e in po if not e.obsolete}
80+
finally:
81+
os.unlink(tmp)
82+
83+
84+
def klasse_von(prozent: float) -> str:
85+
for grenze, name in KLASSEN:
86+
if prozent >= grenze:
87+
return name
88+
return KLASSEN[-1][1]
89+
90+
91+
def uebersetzt(werte) -> int:
92+
return sum(1 for v in werte if v.strip())
93+
94+
95+
def main() -> None:
96+
p = argparse.ArgumentParser(
97+
description=__doc__,
98+
formatter_class=argparse.RawDescriptionHelpFormatter,
99+
)
100+
p.add_argument("alt", nargs="?", default="3.14", help="aelterer Branch")
101+
p.add_argument("neu", nargs="?", default="3.15", help="neuerer Branch")
102+
p.add_argument("--csv", metavar="DATEI", help="Ergebnis zusaetzlich als CSV")
103+
p.add_argument(
104+
"--nur-abweichungen",
105+
action="store_true",
106+
help="deckungsgleiche Dateien in der Tabelle weglassen",
107+
)
108+
args = p.parse_args()
109+
110+
alt_dateien = po_dateien(args.alt)
111+
neu_dateien = po_dateien(args.neu)
112+
alle = sorted(set(alt_dateien) | set(neu_dateien))
113+
print(f"{len(alle)} Dateien werden verglichen ...", file=sys.stderr)
114+
115+
zeilen = []
116+
for nr, pfad in enumerate(alle, 1):
117+
if nr % 50 == 0:
118+
print(f" {nr}/{len(alle)}", file=sys.stderr)
119+
120+
a_blob = alt_dateien.get(pfad)
121+
n_blob = neu_dateien.get(pfad)
122+
123+
if a_blob is None:
124+
n = eintraege(n_blob)
125+
zeilen.append((pfad, 0, len(n), 0, uebersetzt(n.values()),
126+
0, None, f"nur in {args.neu}"))
127+
continue
128+
if n_blob is None:
129+
a = eintraege(a_blob)
130+
zeilen.append((pfad, len(a), 0, uebersetzt(a.values()), 0,
131+
0, None, f"nur in {args.alt}"))
132+
continue
133+
134+
if a_blob == n_blob:
135+
# Gleicher Blob -- Inhalt garantiert identisch, kein Parsen noetig.
136+
a = eintraege(a_blob)
137+
u = uebersetzt(a.values())
138+
zeilen.append((pfad, len(a), len(a), u, u, len(a), 100.0,
139+
"deckungsgleich"))
140+
continue
141+
142+
a = eintraege(a_blob)
143+
n = eintraege(n_blob)
144+
gleich = sum(1 for k, v in n.items() if k in a and a[k] == v)
145+
prozent = 100.0 * gleich / len(n) if n else 0.0
146+
zeilen.append((pfad, len(a), len(n), uebersetzt(a.values()),
147+
uebersetzt(n.values()), gleich, prozent,
148+
klasse_von(prozent)))
149+
150+
# --- Tabelle -----------------------------------------------------------
151+
rang = {name: i for i, (_g, name) in enumerate(KLASSEN)}
152+
rang[f"nur in {args.neu}"] = 90
153+
rang[f"nur in {args.alt}"] = 91
154+
zeilen.sort(key=lambda z: (rang.get(z[7], 99), -(z[6] or 0), z[0]))
155+
156+
kopf = (f"| Datei | Eintraege {args.alt} | Eintraege {args.neu} | "
157+
f"uebers. {args.alt} | uebers. {args.neu} | identisch | "
158+
f"Aehnlichkeit | Klasse |")
159+
print()
160+
print(kopf)
161+
print("|---|---:|---:|---:|---:|---:|---:|---|")
162+
for pfad, ea, en, ua, un, gl, pz, kl in zeilen:
163+
if args.nur_abweichungen and kl == "deckungsgleich":
164+
continue
165+
pzt = "—" if pz is None else f"{pz:.1f} %"
166+
print(f"| `{pfad}` | {ea} | {en} | {ua} | {un} | {gl} | {pzt} | {kl} |")
167+
168+
# --- Zusammenfassung ---------------------------------------------------
169+
print("\n### Zusammenfassung\n")
170+
print("| Klasse | Dateien |")
171+
print("|---|---:|")
172+
for _grenze, name in KLASSEN:
173+
anzahl = sum(1 for z in zeilen if z[7] == name)
174+
print(f"| {name} | {anzahl} |")
175+
for name in (f"nur in {args.neu}", f"nur in {args.alt}"):
176+
anzahl = sum(1 for z in zeilen if z[7] == name)
177+
if anzahl:
178+
print(f"| {name} | {anzahl} |")
179+
print(f"| **gesamt** | **{len(zeilen)}** |")
180+
181+
if args.csv:
182+
with open(args.csv, "w", newline="", encoding="utf-8") as f:
183+
w = csv.writer(f)
184+
w.writerow(["datei", f"eintraege_{args.alt}", f"eintraege_{args.neu}",
185+
f"uebersetzt_{args.alt}", f"uebersetzt_{args.neu}",
186+
"identisch", "aehnlichkeit_prozent", "klasse"])
187+
for z in zeilen:
188+
w.writerow([z[0], z[1], z[2], z[3], z[4], z[5],
189+
"" if z[6] is None else f"{z[6]:.1f}", z[7]])
190+
print(f"\nCSV geschrieben: {args.csv}", file=sys.stderr)
191+
192+
193+
if __name__ == "__main__":
194+
main()

0 commit comments

Comments
 (0)