apply_review.py

Übersetzungsprüfung: wendet die Prüfungsmodule r<JJJJ>_*.py auf translations/<JJJJ>.json an (Ersetzungen bzw. Neufassungen, danach Glossar-Regeln auf jede geprüfte Meldung, Dubletten automatisch) und schreibt die Änderungsliste (uebersetzungspruefung_3301, uebersetzungspruefung_3302, uebersetzungspruefung_3303, uebersetzungspruefung_3304, uebersetzungspruefung_3305, uebersetzungspruefung_3309, uebersetzungspruefung_3310). Dazu dump.py (Original und deutsche Fassung nebeneinander; ohne englischen Text wird ES/FR/PT-BR/RU gezeigt, Dubletten nach Originaltext), cov.py (Vollständigkeit, Jahr über Umgebungsvariable Y), filt.py (Vorfilterung), wkchk.py (Fraktionslisten der Wochenberichte gegen das Original) und mkpkg.py (Korrekturpaket).

Aufruf: python3 translations/review/apply_review.py galnet_raw <JJJJ> (Eingabe ist der Stand vor der Prüfung).

Herunterladen: auf den Dateinamen über dem Code klicken.

apply_review.py

Ablage beim Arbeiten: translations/review/apply_review.py.

apply_review.py
"""apply_review.py RAW JAHR -> wendet die Durchsicht (translations/review/r<JAHR>_*.py) auf translations/<JAHR>.json an
und schreibt die Aenderungsliste translations/review/aenderungen_<JAHR>.txt (Wiki-Syntax).
R[fid] = (Titel, Text, [Aenderungen]) ersetzt die Fassung; None = geprueft, keine inhaltliche Aenderung."""
import json, csv, sys, re, glob, importlib, collections
from pathlib import Path
sys.path.insert(0, 'translations/review')
R_ = Path(sys.argv[1]); y = int(sys.argv[2])
idx = [r for r in csv.DictReader(open(R_ / 'index.csv', encoding='utf-8'), delimiter=';') if int(r['date'][:4]) == y]
byday = collections.defaultdict(list)
for r in idx: byday[r['date'][:10]].append(r['frontier_id'])
PID = {}
for d, fids in byday.items():
    for n, f in enumerate(sorted(fids), 1): PID[f] = f"news:galnet:{d[:4]}:{d[5:7]}:{d[8:10]}_{n:02d}"
DATE = {r['frontier_id']: r['date'][:10] for r in idx}
P = f'translations/{y}.json'
T = json.load(open(P, encoding='utf-8'))
R = {}
for f in sorted(glob.glob(f'translations/review/r{y}_*.py')):
    R.update(importlib.import_module(Path(f).stem).R)
orig = {k: v['body'] for k, v in T.items()}
MISS = []
# Glossar (ki:galnet:glossar): einheitliche Begriffe nach Frontier-DE ab 3306, auf jede geprüfte Meldung angewandt
ART = {'der': 'der Föderalen Flotte', 'die': 'die Föderale Flotte', 'Der': 'Der Föderalen Flotte', 'Die': 'Die Föderale Flotte'}
def _cmdr(m):
    # Singular vor Namen oder nach der/ein/jeder, sonst Plural (deutsch „die Commander“)
    if m[1] or m[2]: return (m[1] or '') + 'Kommandant'
    return 'Kommandanten'
GLOSS = [
    # Behörden wie Frontier-DE: IISS = Imperialer Interner Sicherheitsdienst, FIA = Föderaler Nachrichtendienst, FSS = Föderaler Sicherheitsdienst
    (r'\b(der|Der) Imperial Internal Security Service\b', r'\1 Imperiale Interne Sicherheitsdienst'),
    (r'\bdes Imperial Internal Security Service\b', 'des Imperialen Internen Sicherheitsdienstes'),
    (r'\b(dem|Dem|vom|Vom|zum|beim|den|Den) Imperial Internal Security Service\b', r'\1 Imperialen Internen Sicherheitsdienst'),
    (r'\bImperial Internal Security Service\b', 'Imperialer Interner Sicherheitsdienst'),
    (r'\b(?:des|der) Internen Imperialen Sicherheitsdienst(?:es|s)?\b', 'des Imperialen Internen Sicherheitsdienstes'),
    (r'\bImperiale(\w*) Innere(\w*) Sicherheitsdienst', r'Imperiale\1 Interne\2 Sicherheitsdienst'),
    (r'\b(der|Der) Geheimdienst der Föderation\b', r'\1 Föderale Nachrichtendienst'),
    (r'\b(dem|Dem|vom|Vom|zum|beim|den|Den) Geheimdienst der Föderation\b', r'\1 Föderalen Nachrichtendienst'),
    (r'\bdes [Ff]öderalen Geheimdienst(?:es|s)?\b', 'des Föderalen Nachrichtendienstes'),
    (r'\b[Ff]öderale(n|r)? Geheimdienst(es|s)?\b', lambda m: 'Föderale' + (m[1] or '') + ' Nachrichtendienst' + (m[2] or '')),
    (r'\bProfessor (Katrien )?Rook\b', lambda m: 'Professorin ' + (m[1] or '') + 'Rook'),
    (r'\bFabrikatoren\b', 'Autofabrikatoren'), (r'\bCobalt\b', 'Kobalt'),
    (r'\b(der|Der) Federal Security Service\b', r'\1 Föderale Sicherheitsdienst'),
    (r'\b(dem|Dem|vom|Vom|zum|beim|den|Den) Federal Security Service\b', r'\1 Föderalen Sicherheitsdienst'),
    (r"\b(?:Imperial Dawn|Imperatorendämmerung|(?:Imperialen?|Imperatorischen?) Morgen(?:röte|dämmerung))\b", "Emperor's Dawn"),
    (r"\b(gegen|über|für|an) die Emperor's Dawn\b", r"\1 Emperor's Dawn"),
    (r'^(?:Galaktische Nachrichten|Galactic News):', 'Galaktische News:'),
    (r'^(?:Freelancer Report|Freelance Report|Freiberuflicher Bericht|Freelancer-Bericht):', 'Freelancer-Report:'),
    (r'^(?:Community Goal|Gemeinschaftsziel):', 'Community-Ziel:'), (r'^(?:Powerplay|Machtspiele): (?:Wöchentlicher Bericht|Wochenbericht)$', 'Machtspiele: Wochenbericht'), (r'^Powerplay:', 'Machtspiele:'),
    (r'\bKumo-Besatzung\b', 'Kumo Crew'),
    (r'\bThargoid Raumpocken-', 'Thargoiden-Barnacle-'), (r'\bRaumpocken-', 'Barnacle-'), (r'\bRaumpocken\b', 'Barnacles'), (r'\bRaumpocke\b', 'Barnacle'),
    (r'\bWitch Head Nebel', 'Witch-Head-Nebel'),
    (r'\bInterstellaren? Landwirtschafts-Gesellschaft\b', 'Interstellar Association for Agriculture'), (r'\bILG\b', 'IAA'),
    (r'\bIISD\b', 'IISS'), (r'\bProfessor (Alba )?Tesreau\b', lambda m: 'Professorin ' + (m[1] or '') + 'Tesreau'),
    (r'\bOcellus-Sphäre-Sternenhafen\b', 'Ocellus-Sternenhäfen'),
    (r'\bBarnaklen-', 'Barnacle-'), (r'\b(?:Barnaklen|Baranklen)\b', 'Barnacles'), (r'\bSeepocken\b', 'Barnacles'), (r'\bSeepocke\b', 'Barnacle'), (r'\bPiraten-Lords?\b', lambda m: 'Piratenfürst' + ('en' if m[0].endswith('s') else '')),
    (r'\bImperatorin(?= Arissa)', 'Imperator'), (r'\bPiratenlords\b', 'Piratenfürsten'), (r'\bPiratenlord\b', 'Piratenfürst'),
    (r"\b(?:Entdecker|Forscher)vereinigung\b", "Explorers' Association"), (r"\bExplorer's Association\b", "Explorers' Association"),
    (r'\bSterne?nflughafen(s)?\b', r'Sternenhafen\1'), (r'\bSterne?nflughäfen\b', 'Sternenhäfen'), (r'\bKumo-Crew\b', 'Kumo Crew'),
    (r'\bSchild(?:es)? der Gerechtigkeit\b', 'Shield of Justice'),
    (r'\bKorrigendum\b', 'Corrigendum'), (r'\bLibertas-Kooperative\b', 'Libertas Cooperative'),
    (r'\bDefence Force of Amitrite\b', 'Defence Force of Amitrite'),
    (r'\bCerberus-(?:Seuche|Pest)\b', 'Cerberus-Plage'), (r'\bZerberus-?[Pp]est\b', 'Cerberus-Plage'), (r'\bRaumschiff Eins\b', 'Starship One'),
    (r'\bRaumhafen(s)?\b', r'Sternenhafen\1'), (r'\bRaumhäfen\b', 'Sternenhäfen'), (r'\bAdmiral(s)? der Flotte\b', lambda m: 'Flottenadmiral' + (m[1] or '')), (r'\bSternhäfen\b', 'Sternenhäfen'),
    (r'\b(der|die|Der|Die) (?:Föderationsmarine|Bundesmarine|[Ff]öderalen? Marine|Federal Navy|Föderalen? Navy|Föderal Navy)\b', lambda m: f"{m[1]} {'Föderalen' if m[1].lower() == 'der' else 'Föderale'} Flotte"),
    (r'\b(?:Föderationsmarine|Bundesmarine|[Ff]öderale Marine|Föderal Navy)\b', 'Föderale Flotte'),
    (r'\b(der|die|Der|Die) (?:[Ii]mperialen? Marine)\b', lambda m: f"{m[1]} {'Imperialen' if m[1].lower() == 'der' else 'Imperiale'} Flotte"),
    (r'\b[Ii]mperiale Marine\b', 'Imperiale Flotte'),
    (r'\bKommandeur(?:s)?\b', 'Kommandant'), (r'\bKommandeure(?:n)?\b', 'Kommandanten'),
    (r'(?<!Post )(?<!Post-)\bCommanders\b', 'Kommandanten'), (r'(?<!Post )(?<!Post-)(?<!Wing )\b(der |Der |ein |Ein |jeder |Jeder |dieser |Dieser )?Commander\b(?=( [A-Z0-9]| ?[\'"„“]?[A-Z0-9])?)', _cmdr),
    (r'\bKaiserpalast(?:es|s)?\b', lambda m: 'imperialen Palast' + ('es' if m[0].endswith(('es', 's')) else '')),
    (r'\bKaiserhochzeit\b', 'imperiale Hochzeit'), (r'\bKaiserreich(?:s|es)?\b', 'Imperium'),
    (r'\bkaiserlich(e|en|er|es|em)?\b', lambda m: 'imperial' + (m[1] or '')), (r'\bKaiserlich(e|en|er|es|em)?\b', lambda m: 'Imperial' + (m[1] or '')),
    (r'\bKaisers\b', 'Imperators'), (r'\bKaiser\b', 'Imperator'),
    (r'\bSternhafen', 'Sternenhafen'), (r'\bGalaxis\b', 'Galaxie'),
    (r'\bbesetzten (Welt)?[Rr]aum(s|es)?\b', lambda m: 'besiedelten Raum' + (m[2] or '')),
    (r'\bExplorationsdaten\b', 'Erkundungsdaten'), (r'\bFöderationsraum(s|es)?\b', lambda m: 'föderalen Raum' + (m[1] or '')),
    (r'\b(im|ins|ans|vom|zum|dem|das|des|ein|einem) ((?:[A-Z][\w\'’.+-]*|\d[\w.+-]*)(?: (?:[A-Z][\w\'’.+-]*|\d[\w.+-]*|[a-z]\d[\w-]*))*)-Systems?\b',
     lambda m: f"{m[1]} System{'s' if m[0].endswith('Systems') else ''} {m[2]}"),
]
def gloss(t):
    for a, b in GLOSS: t = re.sub(a, b, t)
    return t.replace('imperialen Palastes', 'imperialen Palastes')
log = []
for fid, v in R.items():
    twins = [k for k in T if k != fid and orig[k] == orig[fid]]
    for k in [fid] + twins:
        T[k]['reviewed'] = True
        if v is not None and v[0] == 'sub':
            for a, b in v[1]:
                if a not in T[k]['body'] and a not in T[k]['title']: MISS.append(f'{k} {a!r}'); continue
                T[k]['body'] = T[k]['body'].replace(a, b); T[k]['title'] = T[k]['title'].replace(a, b)
        elif v is not None:
            T[k]['title'], T[k]['body'] = v[0], v[1].strip()
        T[k]['title'], T[k]['body'] = gloss(T[k]['title']), gloss(T[k]['body'])
    if v: log.append((DATE[fid], fid, T[fid]['title'], v[2], twins))
if MISS: raise SystemExit('Ersetzungen passen nicht:\n' + '\n'.join(MISS))
json.dump(T, open(P, 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
# Aenderungsliste
out = [f"====== Übersetzungsprüfung {y + 1286} ======", "",
       f"Vollständige Durchsicht aller Übersetzungen ohne Frontier-DE gegen das Original (Englisch; wo Frontier keinen englischen Text hat, Spanisch, Französisch oder Russisch; [[ki:galnet:glossar|Glossar]]). "
       f"Geprüft: {sum(1 for t in T.values() if t.get('reviewed'))} von {len(T)} Meldungen, davon {len(log)} geändert. "
       "Fehlerarten: **S** Sinnfehler, **F** Fakt/Name/Zahl, **B** Begriff (Glossar), **Sp** Sprache. "
       "Meldungen, die nur durch das Glossar angeglichen wurden (z. B. Kaiser → Imperator), sind nicht einzeln aufgeführt.", ""]
mon = None
for d, fid, title, notes, twins in sorted(log):
    if d[:7] != mon:
        mon = d[:7]; out += ["", f"===== {mon[5:7]}/{y + 1286} =====", "^ Datum ^ Meldung ^ Änderungen ^"]
    n = ' \\\\ '.join(('**' + x + '**') if x.startswith('S:') else x for x in notes).replace('|', '/')
    extra = f" (+ Dublette {', '.join('[[' + PID[t] + ']]' for t in twins)})" if twins else ''
    out.append(f"| {d[8:10]}.{d[5:7]}. | [[{PID[fid]}|{title}]]{extra} | {n} |")
open(f'translations/review/aenderungen_{y}.txt', 'w', encoding='utf-8').write('\n'.join(out) + '\n')
print(len(R), 'geprüft,', len(log), 'geändert,', sum(1 for _, _, _, n, _ in log for x in n if x.startswith('S:')), 'Sinnfehler-Einträge')

dump.py

Ablage beim Arbeiten: translations/review/dump.py.

dump.py
"""dump.py RAW JAHR MONAT -> Englisch und aktuelle deutsche Fassung nebeneinander (zur Durchsicht)"""
import json, csv, sys
from pathlib import Path
R = Path(sys.argv[1]); y = int(sys.argv[2]); mon = sys.argv[3]
idx = [r for r in csv.DictReader(open(R / 'index.csv', encoding='utf-8'), delimiter=';') if r['date'][:7] == f'{y}-{mon}']
EN = {}
for f in (R / 'EN').glob(f'{y - 1286 + 1286 - 1286 + 3301 - 2015 + y - y}*.json' if False else f'{y + 1286}*.json'):
    for a in json.load(open(f, encoding='utf-8'))['articles']: EN[a['id']] = a
T = json.load(open(f'translations/{y}.json', encoding='utf-8'))
OTHER = {}  # Fallback, wenn Frontier keinen englischen Text hat (z. B. nur ES)
for L in ('ES', 'FR', 'PT-BR', 'RU', 'EN-US'):
    for f in (R / L).glob(f'{y + 1286}*.json'):
        for a in json.load(open(f, encoding='utf-8'))['articles']: OTHER.setdefault(a['id'], (L, a))
seen = {}
for r in idx:
    k = r['frontier_id']
    if k not in T: continue
    e = EN.get(k); lab = 'EN'
    if not e and k in OTHER: lab, e = OTHER[k]
    key = (e['body'] if e else T[k]['body'])[:400]  # Dublette nach Originaltext, nicht nach Übersetzung
    if key in seen: print(f"=== {k} {r['date'][:10]} DUBLETTE von {seen[key]}\n"); continue
    seen[key] = k
    print(f"=== {k} {r['date'][:10]} [{T[k]['source']}]")
    print(f'{lab}:', e['title'] if e else '-'); print(e['body'].strip() if e else '')
    print('DE:', T[k]['title']); print(T[k]['body']); print()

cov.py

Ablage beim Arbeiten: /tmp/claude-0/-home-claude/412360f8-9c0b-55d1-a24e-f4646e8683bb/scratchpad/cov.py.

cov.py
import sys, os, importlib; sys.path[:0]=['translations/review','translations/src']; Y=os.environ.get('Y','2015')
m=sys.argv[1]; S=sys.argv[2]; R={}
for p in sys.argv[3:]+[f"r{Y}_weekly"]: R.update(importlib.import_module(p).R)
ids=[l.split()[1] for l in open(f'{S}/rv_{Y}_{m}.txt') if l.startswith('=== ') and 'DUBLETTE' not in l]
print('fehlt:',[i for i in ids if i not in R]); W=importlib.import_module(f'r{Y}_weekly'); print("extra:",[i for i in R if i not in ids and i not in W.R])

filt.py

Ablage beim Arbeiten: /tmp/claude-0/-home-claude/412360f8-9c0b-55d1-a24e-f4646e8683bb/scratchpad/filt.py.

filt.py
import sys, os, re; sys.path[:0]=['translations/review','translations/src']; Y=os.environ.get('Y','2015')
import importlib, weekly; w=importlib.import_module(f'r{Y}_weekly')
m=sys.argv[1]; S=sys.argv[2]
txt=open(f'{S}/rv_{Y}_{m}.txt').read()
blocks=re.split(r'(?m)^(?==== )', txt)
out=[]
for b in blocks:
    if not b.strip(): continue
    fid=b.split()[1]
    if 'DUBLETTE' in b.split('\n')[0] or fid in w.R: continue
    t=b.split('\n')[1][4:].strip()
    if '[KI]' in b.split('\n')[0] and t in weekly.TITLES: out.append(f"=== {fid} KI-WEEKLY\n\n"); continue
    out.append(b)
open(f'{S}/rf_{Y}_{m}.txt','w').write(''.join(out))
print(m, len(out), sum(len(x) for x in out))

wkchk.py

Ablage beim Arbeiten: /tmp/claude-0/-home-claude/412360f8-9c0b-55d1-a24e-f4646e8683bb/scratchpad/wkchk.py.

wkchk.py
import re, sys
S='/tmp/claude-0/-home-claude/412360f8-9c0b-55d1-a24e-f4646e8683bb/scratchpad'
Y, ms = sys.argv[1], sys.argv[2:]
for m in ms:
    out=[]
    for b in re.split(r'(?m)^(?==== )', open(f'{S}/rf_{Y}_{m}.txt').read()):
        l=b.rstrip('\n').split('\n')
        if len(l)<3 or 'KI-WEEKLY' in l[0]: out.append(b); continue
        i=[n for n,x in enumerate(l) if x.startswith('DE: ')][0]
        de=l[i:]
        if ('Wöchentlicher' in de[0] and 'report' in de[0].lower()) or 'Status-Update' in de[0]:
            bul=[x[2:].strip() for x in de if x.startswith('• ')]
            src=[x.replace('<br />','').strip() for x in l[2:i] if x.replace('<br />','').strip() and not re.search(r'[.:!?]\s*$', x.replace('<br />','').strip()) and len(x.split())<=7]
            src=[re.sub(r'\t',', ',x).strip() for x in src]
            norm=lambda x: re.sub(r'[\s,]+',' ',x).strip()
            ok = [norm(x) for x in bul]==[norm(x) for x in src]
            print(m, l[0].split()[1], 'WEEKLY', 'OK' if ok else f'DIFF {set(src)^set(bul)}')
            continue
        out.append(b)
    open(f'{S}/rg_{Y}_{m}.txt','w').write(''.join(out))

mkpkg.py

Ablage beim Arbeiten: /tmp/claude-0/-home-claude/412360f8-9c0b-55d1-a24e-f4646e8683bb/scratchpad/mkpkg.py.

mkpkg.py
"""mkpkg.py OUT YEAR:OLD:NEW [...] -- Korrekturpaket: geänderte Meldungsseiten, Korrektur-CSV, Prüfungs-/Werkzeugseiten"""
import os, sys, shutil, json, glob, filecmp, csv
S='/tmp/claude-0/-home-claude/412360f8-9c0b-55d1-a24e-f4646e8683bb/scratchpad'
OUT=sys.argv[1]; K=f'{S}/{OUT}'; W=f'{K}/pages/ki/galnet/werkzeuge'
shutil.rmtree(K, ignore_errors=True); os.makedirs(W)
years=[]
for a in sys.argv[2:]:
    y,old,new=a.split(':'); years.append(y); n=0
    for root,_,fs in os.walk(f'{S}/{new}/pages/news/galnet/{y}'):
        for f in fs:
            nf=os.path.join(root,f); of=nf.replace(f'/{new}/',f'/{old}/')
            if not os.path.exists(of) or not filecmp.cmp(of,nf,shallow=False):
                d=nf.replace(f'{S}/{new}/',f'{K}/'); os.makedirs(os.path.dirname(d),exist_ok=True); shutil.copy(nf,d); n+=1
    oc=f'{S}/{old}/galnet_{y}.csv'
    if os.path.exists(oc):
        old_r={r['pid']:r for r in csv.DictReader(open(oc))}
        rows=list(csv.DictReader(open(f'{S}/{new}/galnet_{y}.csv'))); ch=[r for r in rows if old_r.get(r['pid'])!=r]
        if ch:
            with open(f'{K}/galnet_{y}_korrektur.csv','w',newline='') as f:
                w=csv.DictWriter(f,list(rows[0].keys())); w.writeheader(); w.writerows(ch)
    else: ch=[]
    print(y,'Seiten',n,'CSV',len(ch))
shutil.copy('translations/review/glossar.md', f'{K}/pages/ki/galnet/glossar.txt')
def page(name, title, intro, files):
    out=[f'====== {title} ======','',intro,'',"Herunterladen: auf den Dateinamen über dem Code klicken.",'']
    for path, lang in files:
        fn=os.path.basename(path)
        out += [f"===== {fn} =====", f"Ablage beim Arbeiten: ''{path}''.", '', f'<code {lang} {fn}>', open(path, encoding='utf-8').read().rstrip('\n'), '

', ] open(f'{W}/{name}.txt','w',encoding='utf-8').write('\n'.join(out)) done=sorted(int(f[-8:-4]) for f in glob.glob('translations/review/aenderungen_*.txt')) links=', '.join(f'uebersetzungspruefung_{y+1286}' for y in done) page('apply_review','apply_review.py',f„Übersetzungsprüfung: wendet die Prüfungsmodule ''r<JJJJ>_*.py auf translations/<JJJJ>.json an (Ersetzungen bzw. Neufassungen, danach Glossar-Regeln auf jede geprüfte Meldung, Dubletten automatisch) und schreibt die Änderungsliste ({links}). Dazu dump.py (Original und deutsche Fassung nebeneinander; ohne englischen Text wird ES/FR/PT-BR/RU gezeigt, Dubletten nach Originaltext), cov.py (Vollständigkeit, Jahr über Umgebungsvariable Y), filt.py (Vorfilterung), wkchk.py (Fraktionslisten der Wochenberichte gegen das Original) und mkpkg.py (Korrekturpaket).\n\nAufruf: python3 translations/review/apply_review.py galnet_raw <JJJJ> (Eingabe ist der Stand vor der Prüfung).“,

   [('translations/review/apply_review.py','python'),('translations/review/dump.py','python'),(f'{S}/cov.py','python'),(f'{S}/filt.py','python'),(f'{S}/wkchk.py','python'),(f'{S}/mkpkg.py','python')])

for y in years:

  j=int(y)+1286
  shutil.copy(f'translations/review/aenderungen_{y}.txt', f'{K}/pages/ki/galnet/uebersetzungspruefung_{j}.txt')
  mods=sorted(glob.glob(f'translations/review/r{y}_*.py'))
  page(f'review_{y}',f'Prüfungsmodule {j}',f"Ergebnis der vollständigen Durchsicht {j}: ''R[frontier_id] = None'' (geprüft, in Ordnung), ''('sub', [(alt, neu), …], Notizen)'' (gezielte Ersetzungen) oder ''(Titel, Text, Notizen)'' (Neufassung). Notizen: **S** Sinnfehler, **F** Fakt/Name/Zahl, **B** Begriff, **Sp** Sprache. ''r{y}_weekly.py'' setzt die alten Wochenberichte mit ''weekly.py'' neu und wird zuletzt geladen.",
       [(m,'python') for m in mods])
  T=json.load(open(f'translations/{y}.json')); src={}
  for v in T.values(): src[v['source']]=src.get(v['source'],0)+1
  open(f'{W}/translations_{y}.txt','w',encoding='utf-8').write('\n'.join([f'====== {y}.json ======','',
   f"Übersetzungen {j} (keine Frontier-DE): {src.get('eigene',0)} × Oliver, {src.get('KI',0)} × Claude. **Stand nach der vollständigen Übersetzungsprüfung** ([[ki:galnet:uebersetzungspruefung_{j}]]): alle Meldungen gegen das Original gelesen, nach [[ki:galnet:glossar]] vereinheitlicht.",
   '',f"Ablage beim Arbeiten: ''translations/{y}.json''. Herunterladen: auf den Dateinamen über dem Code klicken.",'',f'<code json {y}.json>', open(f'translations/{y}.json',encoding='utf-8').read().rstrip('\n'), '</code>','']))

</code>