forked from fa/breadcrumb-the-shire
docs: replace ASCII umlaut fallbacks with proper äöüß across all .md
Most German docs were written with `ue`/`ae`/`oe`/`ss` ASCII fallbacks (`fuer`, `aenderung`, `koennen`, `gemaess`) — relic from older toolchain constraints. Replaced 237 occurrences across 38 .md files with proper umlauts and ß so the docs read naturally. Substitutions are constrained to plain prose: fenced code blocks (```...```), inline code spans (`...`), markdown link targets `[..](..)`, and HTML comments are preserved verbatim. Path references like `docs/howto-erste-aenderung.md` keep their original (umlaut-replaced) filenames — only the surrounding prose is normalised. Tool: bin/fix-md-umlauts.py (idempotent — no-op on already-clean files). Re-runnable if ASCII fallbacks creep back in via copy-paste. Doc-link-check + doc-drift-check stay green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
262
bin/fix-md-umlauts.py
Executable file
262
bin/fix-md-umlauts.py
Executable file
@@ -0,0 +1,262 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Replace ASCII-fallback German umlauts (ue, ae, oe, ss) with proper umlauts
|
||||
inside .md prose, while preserving:
|
||||
- fenced code blocks (``` ... ```)
|
||||
- inline code spans (` ... `)
|
||||
- markdown link targets ([label](path)) — only the (path) part is preserved
|
||||
- HTML comments (<!-- ... -->)
|
||||
|
||||
Usage: python3 bin/fix-md-umlauts.py <file.md> [<file.md> ...]
|
||||
--dry-run print proposed changes per file (default: write in place)
|
||||
--check exit 1 if any file would change (CI mode)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Order matters: longer patterns first to prevent partial matches
|
||||
# (e.g. "aenderung" must be tried before "aender" so prose-ending stays clean).
|
||||
SUBSTITUTIONS: list[tuple[str, str]] = [
|
||||
# ß / ss
|
||||
("massgeblich", "maßgeblich"),
|
||||
("Massgeblich", "Maßgeblich"),
|
||||
("gemaess", "gemäß"),
|
||||
("Gemaess", "Gemäß"),
|
||||
("regelmaessig", "regelmäßig"),
|
||||
("Regelmaessig", "Regelmäßig"),
|
||||
("groesst", "größt"),
|
||||
("Groesst", "Größt"),
|
||||
# ä – longer first
|
||||
("aenderung", "änderung"),
|
||||
("Aenderung", "Änderung"),
|
||||
("aenderbar", "änderbar"),
|
||||
("aendert", "ändert"),
|
||||
("aendern", "ändern"),
|
||||
("Aender", "Änder"),
|
||||
("aender", "änder"),
|
||||
("naechst", "nächst"),
|
||||
("Naechst", "Nächst"),
|
||||
("naemlich", "nämlich"),
|
||||
("Naemlich", "Nämlich"),
|
||||
("waehrend", "während"),
|
||||
("Waehrend", "Während"),
|
||||
("waehl", "wähl"),
|
||||
("Waehl", "Wähl"),
|
||||
("haeufig", "häufig"),
|
||||
("Haeufig", "Häufig"),
|
||||
("spaeter", "später"),
|
||||
("Spaeter", "Später"),
|
||||
("aufzaehl", "aufzähl"),
|
||||
("Aufzaehl", "Aufzähl"),
|
||||
("gefaehr", "gefähr"),
|
||||
("Gefaehr", "Gefähr"),
|
||||
("erlaeut", "erläut"),
|
||||
("Erlaeut", "Erläut"),
|
||||
("bestaetig", "bestätig"),
|
||||
("Bestaetig", "Bestätig"),
|
||||
("Geschaeft", "Geschäft"),
|
||||
("geschaeft", "geschäft"),
|
||||
# ü – longer first
|
||||
("ausfuehr", "ausführ"),
|
||||
("Ausfuehr", "Ausführ"),
|
||||
("durchfuehr", "durchführ"),
|
||||
("Durchfuehr", "Durchführ"),
|
||||
("einfuehr", "einführ"),
|
||||
("Einfuehr", "Einführ"),
|
||||
("fuehrun", "führun"),
|
||||
("Fuehrun", "Führun"),
|
||||
("fuehrt", "führt"),
|
||||
("fuehren", "führen"),
|
||||
("Fuehren", "Führen"),
|
||||
("verfuegb", "verfügb"),
|
||||
("Verfuegb", "Verfügb"),
|
||||
("unterstuetz", "unterstütz"),
|
||||
("Unterstuetz", "Unterstütz"),
|
||||
("nuetzlich", "nützlich"),
|
||||
("Nuetzlich", "Nützlich"),
|
||||
("Schluess", "Schlüss"),
|
||||
("schluess", "schlüss"),
|
||||
("wuerde", "würde"),
|
||||
("Wuerde", "Würde"),
|
||||
("wuerden", "würden"),
|
||||
("MUESSEN", "MÜSSEN"),
|
||||
("muessen", "müssen"),
|
||||
("Muessen", "Müssen"),
|
||||
("muess", "müss"),
|
||||
("Muess", "Müss"),
|
||||
("KOENNEN", "KÖNNEN"),
|
||||
("koennen", "können"),
|
||||
("Koennen", "Können"),
|
||||
("koenn", "könn"),
|
||||
("Koenn", "Könn"),
|
||||
("natuerli", "natürli"),
|
||||
("Natuerli", "Natürli"),
|
||||
("tatsaechli", "tatsächli"),
|
||||
("Tatsaechli", "Tatsächli"),
|
||||
("aehnlich", "ähnlich"),
|
||||
("Aehnlich", "Ähnlich"),
|
||||
("standardmaessig", "standardmäßig"),
|
||||
("Standardmaessig", "Standardmäßig"),
|
||||
("gleichmaessig", "gleichmäßig"),
|
||||
("Gleichmaessig", "Gleichmäßig"),
|
||||
("zugaengli", "zugängli"),
|
||||
("Zugaengli", "Zugängli"),
|
||||
("uebersicht", "übersicht"),
|
||||
("Uebersicht", "Übersicht"),
|
||||
("uebersetzun", "übersetzun"),
|
||||
("Uebersetzun", "Übersetzun"),
|
||||
("uebernahme", "übernahme"),
|
||||
("Uebernahme", "Übernahme"),
|
||||
("uebernehm", "übernehm"),
|
||||
("Uebernehm", "Übernehm"),
|
||||
("hoeher", "höher"),
|
||||
("Hoeher", "Höher"),
|
||||
("pruefen", "prüfen"),
|
||||
("Pruefen", "Prüfen"),
|
||||
("prueft", "prüft"),
|
||||
("Prueft", "Prüft"),
|
||||
("Pruefung", "Prüfung"),
|
||||
("pruefung", "prüfung"),
|
||||
("pruef", "prüf"),
|
||||
("Pruef", "Prüf"),
|
||||
("ueber", "über"),
|
||||
("Ueber", "Über"),
|
||||
("fuer", "für"),
|
||||
# ö
|
||||
("oeffn", "öffn"),
|
||||
("Oeffn", "Öffn"),
|
||||
("loescht", "löscht"),
|
||||
("Loescht", "Löscht"),
|
||||
("loeschen", "löschen"),
|
||||
("Loeschen", "Löschen"),
|
||||
("loesung", "lösung"),
|
||||
("Loesung", "Lösung"),
|
||||
("moegli", "mögli"),
|
||||
("Moegli", "Mögli"),
|
||||
("moeglich", "möglich"),
|
||||
("Moeglich", "Möglich"),
|
||||
("noetig", "nötig"),
|
||||
("Noetig", "Nötig"),
|
||||
("hoer", "hör"), # gehör, höher
|
||||
("Hoer", "Hör"),
|
||||
]
|
||||
|
||||
# Regex that splits a line into "preserve" tokens and replaceable text.
|
||||
# Order matters: longer / more specific tokens first.
|
||||
TOKENIZER = re.compile(
|
||||
r"""
|
||||
( `[^`\n]*` ) # inline code: `...`
|
||||
| ( !?\[[^\]]*\]\([^)]*\) ) # full markdown link [label](target)
|
||||
| ( <!--.*?--> ) # HTML comment
|
||||
""",
|
||||
re.VERBOSE | re.DOTALL,
|
||||
)
|
||||
|
||||
|
||||
def transform_text(text: str) -> str:
|
||||
"""Apply substitutions to non-preserved spans only."""
|
||||
for needle, replacement in SUBSTITUTIONS:
|
||||
text = text.replace(needle, replacement)
|
||||
return text
|
||||
|
||||
|
||||
def transform_line(line: str) -> str:
|
||||
"""Tokenise a line into [preserved | text | preserved | text | ...] and replace only text segments."""
|
||||
out: list[str] = []
|
||||
pos = 0
|
||||
for match in TOKENIZER.finditer(line):
|
||||
# Replaceable run before the preserved span
|
||||
if match.start() > pos:
|
||||
out.append(transform_text(line[pos : match.start()]))
|
||||
out.append(match.group(0)) # keep preserved span verbatim
|
||||
pos = match.end()
|
||||
# Tail after last preserved span
|
||||
if pos < len(line):
|
||||
out.append(transform_text(line[pos:]))
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def transform_file(content: str) -> str:
|
||||
"""Walk lines, skip fenced code blocks, transform the rest line-by-line."""
|
||||
out_lines: list[str] = []
|
||||
in_fence = False
|
||||
fence_marker = ""
|
||||
|
||||
for line in content.splitlines(keepends=True):
|
||||
stripped = line.lstrip()
|
||||
|
||||
if in_fence:
|
||||
out_lines.append(line)
|
||||
if stripped.startswith(fence_marker):
|
||||
in_fence = False
|
||||
fence_marker = ""
|
||||
continue
|
||||
|
||||
# Detect fenced code start (``` or ~~~)
|
||||
if stripped.startswith("```"):
|
||||
in_fence = True
|
||||
fence_marker = "```"
|
||||
out_lines.append(line)
|
||||
continue
|
||||
if stripped.startswith("~~~"):
|
||||
in_fence = True
|
||||
fence_marker = "~~~"
|
||||
out_lines.append(line)
|
||||
continue
|
||||
|
||||
out_lines.append(transform_line(line))
|
||||
|
||||
return "".join(out_lines)
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
args = list(argv[1:])
|
||||
dry_run = False
|
||||
check = False
|
||||
while args and args[0].startswith("--"):
|
||||
flag = args.pop(0)
|
||||
if flag == "--dry-run":
|
||||
dry_run = True
|
||||
elif flag == "--check":
|
||||
check = True
|
||||
elif flag == "--":
|
||||
break
|
||||
else:
|
||||
print(f"Unknown flag: {flag}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
if not args:
|
||||
print(__doc__.strip(), file=sys.stderr)
|
||||
return 2
|
||||
|
||||
changed_files: list[Path] = []
|
||||
for raw in args:
|
||||
path = Path(raw)
|
||||
if not path.is_file():
|
||||
print(f"Skipping (not a file): {path}", file=sys.stderr)
|
||||
continue
|
||||
original = path.read_text(encoding="utf-8")
|
||||
transformed = transform_file(original)
|
||||
if transformed != original:
|
||||
changed_files.append(path)
|
||||
if dry_run or check:
|
||||
# Brief summary; full diff is what `git diff` shows after a real run.
|
||||
added = sum(1 for c in transformed if c in "äöüÄÖÜß") - sum(
|
||||
1 for c in original if c in "äöüÄÖÜß"
|
||||
)
|
||||
print(f"WOULD CHANGE {path} (+{added} umlauts)")
|
||||
else:
|
||||
path.write_text(transformed, encoding="utf-8")
|
||||
print(f"updated {path}")
|
||||
|
||||
if check and changed_files:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
Reference in New Issue
Block a user