~/german-cheatsheet/generate_cheatsheet.py
#!/usr/bin/env python3
"""
German Grammar Cheat Sheet A1-B1 (Goethe Exam Prep)
Printable A4 PDF — multi-page, colour-coded, exam-focused
"""
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import mm
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER, TA_LEFT
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
PageBreak, HRFlowable, KeepTogether
)
from reportlab.lib.colors import HexColor
# ── Colour Palette ────────────────────────────────────────────────────────────
C_BLACK = HexColor("#1A1A2E")
C_DARK_BLUE = HexColor("#16213E")
C_MID_BLUE = HexColor("#0F3460")
C_ACCENT = HexColor("#E94560") # red-pink accent
C_A1 = HexColor("#27AE60") # green → A1
C_A2 = HexColor("#2980B9") # blue → A2
C_B1 = HexColor("#8E44AD") # purple → B1
C_HEADER_BG = HexColor("#1A1A2E")
C_STRIPE1 = HexColor("#F2F7FF")
C_STRIPE2 = HexColor("#FFFFFF")
C_YELLOW = HexColor("#F39C12")
C_LIGHT_GREY = HexColor("#ECF0F1")
C_TIP = HexColor("#FEF9E7")
C_TIP_BORDER = HexColor("#F39C12")
PAGE_W, PAGE_H = A4
MARGIN = 12 * mm
# ── Document ──────────────────────────────────────────────────────────────────
OUTPUT = "/home/daytona/workspace/german-cheatsheet/German_Grammar_A1-B1_Cheatsheet.pdf"
doc = SimpleDocTemplate(
OUTPUT,
pagesize=A4,
leftMargin=MARGIN, rightMargin=MARGIN,
topMargin=14*mm, bottomMargin=12*mm,
title="German Grammar Cheat Sheet A1–B1",
author="Goethe Exam Prep",
)
# ── Styles ────────────────────────────────────────────────────────────────────
styles = getSampleStyleSheet()
def S(name, **kw):
return ParagraphStyle(name, **kw)
style_cover_title = S("CoverTitle", fontSize=34, leading=40, textColor=colors.white,
alignment=TA_CENTER, fontName="Helvetica-Bold", spaceAfter=6)
style_cover_sub = S("CoverSub", fontSize=15, leading=20, textColor=HexColor("#BDC3C7"),
alignment=TA_CENTER, fontName="Helvetica", spaceAfter=4)
style_cover_note = S("CoverNote", fontSize=11, leading=14, textColor=HexColor("#ECF0F1"),
alignment=TA_CENTER, fontName="Helvetica-Oblique")
style_section = S("Section", fontSize=13, leading=16, textColor=colors.white,
fontName="Helvetica-Bold", spaceAfter=2, spaceBefore=6)
style_subsection = S("SubSection", fontSize=10, leading=13, textColor=C_DARK_BLUE,
fontName="Helvetica-Bold", spaceAfter=2, spaceBefore=4)
style_body = S("Body", fontSize=8.5, leading=11, textColor=C_BLACK,
fontName="Helvetica", spaceAfter=1)
style_tip = S("Tip", fontSize=8, leading=11, textColor=HexColor("#784212"),
fontName="Helvetica-Oblique", spaceAfter=1)
style_small = S("Small", fontSize=7.5, leading=10, textColor=C_BLACK,
fontName="Helvetica")
style_footer = S("Footer", fontSize=7, leading=9, textColor=HexColor("#7F8C8D"),
alignment=TA_CENTER, fontName="Helvetica-Oblique")
# ── Helper: Section Banner ────────────────────────────────────────────────────
def section_banner(text, bg=C_HEADER_BG, fg=colors.white, level_color=None):
display = text
elems = []
data = [[Paragraph(f'<font color="white"><b>{display}</b></font>', style_section)]]
tbl = Table(data, colWidths=[PAGE_W - 2*MARGIN])
tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), bg),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 8),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("ROWBACKGROUNDS", (0,0), (-1,-1), [bg]),
]))
elems.append(tbl)
elems.append(Spacer(1, 3))
return elems
def level_badge(level):
"""Returns coloured inline badge text."""
clr = {"A1": "#27AE60", "A2": "#2980B9", "B1": "#8E44AD"}.get(level, "#555")
return f'<font color="{clr}"><b>[{level}]</b></font>'
# ── Helper: Grammar Table ─────────────────────────────────────────────────────
def grammar_table(headers, rows, col_widths=None, stripe=True):
avail = PAGE_W - 2*MARGIN
if col_widths is None:
n = len(headers)
col_widths = [avail / n] * n
header_row = [Paragraph(f'<font color="white"><b>{h}</b></font>',
S("TH", fontSize=8, leading=10, textColor=colors.white,
fontName="Helvetica-Bold", alignment=TA_CENTER))
for h in headers]
data = [header_row]
for row in rows:
data.append([Paragraph(str(cell), style_small) for cell in row])
tbl = Table(data, colWidths=col_widths, repeatRows=1)
ts = [
("BACKGROUND", (0,0), (-1,0), C_MID_BLUE),
("TEXTCOLOR", (0,0), (-1,0), colors.white),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("FONTSIZE", (0,0), (-1,-1), 8),
("LEADING", (0,0), (-1,-1), 10),
("LEFTPADDING", (0,0), (-1,-1), 4),
("RIGHTPADDING", (0,0), (-1,-1), 4),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING", (0,0), (-1,-1), 3),
("GRID", (0,0), (-1,-1), 0.5, HexColor("#BDC3C7")),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
]
if stripe:
for i in range(1, len(data)):
bg = C_STRIPE1 if i % 2 == 1 else C_STRIPE2
ts.append(("BACKGROUND", (0,i), (-1,i), bg))
tbl.setStyle(TableStyle(ts))
return tbl
def tip_box(text):
data = [[Paragraph(f"<b>Tip:</b> {text}", style_tip)]]
tbl = Table(data, colWidths=[PAGE_W - 2*MARGIN - 4])
tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), C_TIP),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 8),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("BOX", (0,0), (-1,-1), 1, C_TIP_BORDER),
("ROUNDEDCORNERS", [4]),
]))
return tbl
# ═══════════════════════════════════════════════════════════════════════════════
# CONTENT
# ═══════════════════════════════════════════════════════════════════════════════
story = []
# ─── COVER PAGE ───────────────────────────────────────────────────────────────
# Full-page dark background table
cover_content = [
Spacer(1, 30*mm),
Paragraph("🇩🇪 GERMAN GRAMMAR", style_cover_title),
Paragraph("CHEAT SHEET", S("CS2", fontSize=28, leading=34, textColor=C_ACCENT,
alignment=TA_CENTER, fontName="Helvetica-Bold")),
Spacer(1, 6*mm),
Paragraph("Goethe-Zertifikat A1 · A2 · B1", style_cover_sub),
Spacer(1, 4*mm),
HRFlowable(width="60%", thickness=1, color=C_ACCENT, hAlign="CENTER"),
Spacer(1, 6*mm),
Paragraph("Complete Grammar Reference · Exam Templates · Pronunciation Guide",
style_cover_note),
Spacer(1, 4*mm),
Paragraph("All four skills: Reading · Listening · Writing · Speaking",
style_cover_note),
Spacer(1, 30*mm),
Paragraph(
'<font color="#27AE60"><b>A1</b></font> Beginner '
'<font color="#2980B9"><b>A2</b></font> Pre-Intermediate '
'<font color="#8E44AD"><b>B1</b></font> Independent User',
S("Lvls", fontSize=12, leading=16, textColor=colors.white,
alignment=TA_CENTER, fontName="Helvetica-Bold")),
]
cover_data = [[c] for c in cover_content]
# Use a simple approach: just add content with dark background on whole page
cover_bg = Table([[Spacer(1, 0)]], colWidths=[PAGE_W - 2*MARGIN], rowHeights=[PAGE_H - 26*mm])
cover_bg.setStyle(TableStyle([("BACKGROUND", (0,0), (-1,-1), C_DARK_BLUE)]))
for item in cover_content:
story.append(item)
story.append(PageBreak())
# ─── PAGE 2: ALPHABET & PRONUNCIATION ────────────────────────────────────────
story += section_banner("1. ALPHABET & PRONUNCIATION " + level_badge("A1"), bg=C_A1)
pron_data = [
["Letters / Sound", "Rule", "Example", "English Guide"],
["W", "Sounds like English V", "Wasser (water)", "VAH-ser"],
["V", "Sounds like English F", "Vater (father)", "FAH-ter"],
["Z", "Sounds like TS", "Zeit (time)", "TSYTE"],
["S + vowel", "Sounds like English Z", "sehen (to see)", "ZAY-en"],
["ss / ss", "Sharp S (never buzzy)", "Wasser, Strasse", "VAH-ser"],
["sp / st (start)", "SHP / SHT", "sprechen, Stein", "SHPREH-khen, SHTYNE"],
["ch (after a/o/u)", "Guttural — like Scottish loch", "Buch, auch", "bookh, owkh"],
["ch (after e/i)", "Soft hiss — like huge", "ich, nicht", "ikh, nikht"],
["ei", "Long 'eye'", "mein, nein", "mine, nine"],
["ie", "Long 'ee'", "Bier, sie", "beer, zee"],
["eu / au", "'oy' / 'ow'", "neu, Haus", "noy, howss"],
["a with umlaut (ae)", "'air'", "Madchen", "MAYD-khen"],
["o with umlaut (oe)", "'ur' — round lips", "schon", "shurn"],
["u with umlaut (ue)", "'ee' — round lips", "uber", "UE-ber"],
["-ig (word end)", "'ikh'", "richtig, lustig", "RIK-tikh, LUS-tikh"],
["Final b/d/g", "Devoice to p/t/k", "Hund, Tag", "hoont, tahk"],
["r (end of word)", "Soft 'ah'", "Vater, Wasser", "FAH-tah, VAH-sah"],
["j", "Like English Y", "ja, Jahr", "yah, yahr"],
["qu", "Like KV", "Qualitat", "kva-li-TAYT"],
]
story.append(grammar_table(
pron_data[0], pron_data[1:],
col_widths=[45*mm, 60*mm, 45*mm, 40*mm]
))
story.append(Spacer(1, 3))
story.append(tip_box("Every letter in German is pronounced — unlike English, there are almost NO silent letters. Speak what you see!"))
story.append(Spacer(1, 4))
# ─── PAGE 2 cont: VERB CONJUGATION ───────────────────────────────────────────
story += section_banner("2. VERB CONJUGATION — PRESENT TENSE " + level_badge("A1"), bg=C_A1)
conj_data = [
["Pronoun", "sein (be)", "haben (have)", "machen (do)", "gehen (go)", "kommen (come)"],
["ich", "bin", "habe", "mache", "gehe", "komme"],
["du", "bist", "hast", "machst", "gehst", "kommst"],
["er/sie/es", "ist", "hat", "macht", "geht", "kommt"],
["wir", "sind", "haben", "machen", "gehen", "kommen"],
["ihr", "seid", "habt", "macht", "geht", "kommt"],
["sie/Sie", "sind", "haben", "machen", "gehen", "kommen"],
]
avail = PAGE_W - 2*MARGIN
story.append(grammar_table(
conj_data[0], conj_data[1:],
col_widths=[avail*0.14, avail*0.17, avail*0.17, avail*0.17, avail*0.17, avail*0.18]
))
story.append(Spacer(1,3))
story.append(tip_box("Regular -en verbs: remove -en, then add: -e / -st / -t / -en / -t / -en"))
story.append(Spacer(1,4))
# ARTICLES TABLE
story += section_banner("3. ARTICLES — DER / DIE / DAS " + level_badge("A1") + " " + level_badge("A2"), bg=C_A2)
art_data = [
["Case", "Use", "Masc. (der)", "Fem. (die)", "Neuter (das)", "Plural (die)"],
["Nominative", "Subject", "der / ein", "die / eine", "das / ein", "die / —"],
["Accusative", "Direct Object", "den / einen", "die / eine", "das / ein", "die / —"],
["Dative", "Indirect Object", "dem / einem", "der / einer", "dem / einem", "den / —"],
["Genitive", "Possession", "des / eines", "der / einer", "des / eines", "der / —"],
]
story.append(grammar_table(
art_data[0], art_data[1:],
col_widths=[30*mm, 38*mm, 32*mm, 28*mm, 32*mm, 30*mm]
))
story.append(Spacer(1,3))
story.append(tip_box("Memory trick — only ACCUSATIVE changes the masculine: der → DEN, ein → EINEN. All others stay the same!"))
story.append(PageBreak())
# ─── PAGE 3: MODAL VERBS ─────────────────────────────────────────────────────
story += section_banner("4. MODAL VERBS " + level_badge("A2"), bg=C_A2)
modal_data = [
["Pronoun", "konnen (can)", "mussen (must)", "wollen (want)", "durfen (may)", "sollen (shall)", "mogen (like)"],
["ich", "kann", "muss", "will", "darf", "soll", "mag"],
["du", "kannst", "musst", "willst", "darfst", "sollst", "magst"],
["er/sie/es", "kann", "muss", "will", "darf", "soll", "mag"],
["wir", "konnen", "mussen", "wollen", "durfen", "sollen", "mogen"],
["ihr", "konnt", "musst", "wollt", "durft", "sollt", "mogt"],
["sie/Sie", "konnen", "mussen", "wollen", "durfen", "sollen", "mogen"],
]
avail = PAGE_W - 2*MARGIN
cw = avail / 7
story.append(grammar_table(modal_data[0], modal_data[1:], col_widths=[cw]*7))
story.append(Spacer(1,3))
story.append(tip_box("Modal verb ALWAYS in position 2; infinitive goes to END: Ich KANN heute nicht KOMMEN."))
story.append(Spacer(1,4))
# PERFEKT TENSE
story += section_banner("5. PERFEKT TENSE (Past) " + level_badge("A2"), bg=C_A2)
perf_data = [
["Pattern", "When to Use", "Formula", "Example"],
["haben + Partizip II", "Most verbs (transitive, non-motion)", "Ich habe + [ge...t/en]", "Ich habe gegessen. (I ate)"],
["sein + Partizip II", "Motion verbs & change of state", "Ich bin + [ge...en]", "Ich bin gegangen. (I went)"],
]
story.append(grammar_table(perf_data[0], perf_data[1:], col_widths=[44*mm, 58*mm, 52*mm, 56*mm]))
story.append(Spacer(1,3))
partizip_data = [
["Infinitive", "Partizip II", "Haben/Sein", "Infinitive", "Partizip II", "Haben/Sein"],
["machen", "gemacht", "haben", "gehen", "gegangen", "sein"],
["spielen", "gespielt", "haben", "fahren", "gefahren", "sein"],
["kaufen", "gekauft", "haben", "kommen", "gekommen", "sein"],
["essen", "gegessen", "haben", "laufen", "gelaufen", "sein"],
["trinken", "getrunken", "haben", "fliegen", "geflogen", "sein"],
["sehen", "gesehen", "haben", "bleiben", "geblieben", "sein"],
["lesen", "gelesen", "haben", "werden", "geworden", "sein"],
["schreiben", "geschrieben", "haben", "steigen", "gestiegen", "sein"],
["arbeiten", "gearbeitet", "haben", "reisen", "gereist", "sein"],
]
cw2 = avail / 6
story.append(grammar_table(partizip_data[0], partizip_data[1:], col_widths=[cw2]*6))
story.append(Spacer(1,3))
story.append(tip_box("Regular Partizip II = ge + stem + t | Irregular = ge + changed stem + en | Verbs with be-/ver-/er- prefix: NO 'ge'!"))
story.append(Spacer(1,4))
# PRATERITUM
story += section_banner("6. PRATERITUM — Simple Past " + level_badge("B1"), bg=C_B1)
prat_data = [
["Verb", "ich", "du", "er/sie/es", "wir", "ihr", "sie/Sie"],
["sein (be)", "war", "warst", "war", "waren", "wart", "waren"],
["haben (have)", "hatte", "hattest", "hatte", "hatten", "hattet", "hatten"],
["gehen (go)", "ging", "gingst", "ging", "gingen", "gingt", "gingen"],
["kommen (come)", "kam", "kamst", "kam", "kamen", "kamt", "kamen"],
["fahren (drive)", "fuhr", "fuhrst", "fuhr", "fuhren", "fuhrt", "fuhren"],
["sehen (see)", "sah", "sahst", "sah", "sahen", "saht", "sahen"],
["nehmen (take)", "nahm", "nahmst", "nahm", "nahmen", "nahmt", "nahmen"],
["wissen (know)", "wusste", "wusstest", "wusste", "wussten", "wusstet", "wussten"],
]
cw3 = [38*mm] + [(avail - 38*mm)/6]*6
story.append(grammar_table(prat_data[0], prat_data[1:], col_widths=cw3))
story.append(Spacer(1,3))
story.append(tip_box("In SPOKEN German, use Perfekt for almost everything. Use Prateritum mainly for: sein, haben, modal verbs, and formal/written German."))
story.append(PageBreak())
# ─── PAGE 4: SENTENCE STRUCTURE ──────────────────────────────────────────────
story += section_banner("7. SENTENCE STRUCTURE & WORD ORDER " + level_badge("A1") + "–" + level_badge("B1"), bg=C_MID_BLUE)
word_order_data = [
["Pattern", "Rule", "Example"],
["Main Clause", "Verb always in POSITION 2", "Ich lerne jeden Tag Deutsch."],
["Yes/No Question", "Verb comes FIRST", "Sprechen Sie Englisch?"],
["W-Question", "W-word + Verb + Subject", "Wo wohnen Sie?"],
["Inversion (adverb first)", "Adverb + Verb + Subject", "Heute gehe ich ins Kino."],
["weil (because)", "Verb goes to END", "Ich lerne, weil es wichtig IST."],
["dass (that)", "Verb goes to END", "Ich denke, dass du Recht HAST."],
["obwohl (although)", "Verb goes to END", "Obwohl es schwer IST, lerne ich."],
["wenn (when/if)", "Verb goes to END", "Wenn ich Zeit HABE, lese ich."],
["als (when — past)", "Verb goes to END", "Als ich jung WAR, spielte ich Fussball."],
["Relative clause", "Verb goes to END", "Der Mann, der hier WOHNT, ist nett."],
["Konjunktiv II", "wurden + infinitive at END", "Ich wurden gern nach Berlin FAHREN."],
["Perfekt", "haben/sein + Partizip II at END", "Ich habe Deutsch GELERNT."],
["Separable verb", "Prefix goes to END", "Ich stehe um 7 Uhr AUF."],
]
story.append(grammar_table(
word_order_data[0], word_order_data[1:],
col_widths=[48*mm, 65*mm, 77*mm]
))
story.append(Spacer(1,4))
# KONJUNKTIV II
story += section_banner("8. KONJUNKTIV II — Wishes & Polite Requests " + level_badge("B1"), bg=C_B1)
konj_data = [
["Verb", "Konjunktiv II Form", "Example Sentence", "Meaning"],
["sein", "ware", "Ich ware gern Arzt.", "I would like to be a doctor."],
["haben", "hatte", "Ich hatte gern mehr Zeit.", "I would like to have more time."],
["werden", "wurde", "Ich wurde reisen.", "I would travel."],
["konnen", "konnte", "Konnten Sie mir helfen?", "Could you help me?"],
["mussen", "musste", "Er musste mehr lernen.", "He would have to study more."],
["wollen", "wollte", "Sie wollte nach Berlin.", "She wanted to go to Berlin."],
["durfen", "durfte", "Durfte ich fragen?", "Might I ask?"],
]
cw4 = [28*mm, 38*mm, 75*mm, 50*mm]
story.append(grammar_table(konj_data[0], konj_data[1:], col_widths=cw4))
story.append(Spacer(1,3))
story.append(tip_box("Most common Konjunktiv II: wurden + infinitive (polite/hypothetical). Use hatte/ware/konnte for most other situations."))
story.append(Spacer(1,4))
# CONJUNCTIONS TABLE
story += section_banner("9. CONJUNCTIONS " + level_badge("A2") + "–" + level_badge("B1"), bg=C_A2)
conj_types = [
["Type", "Conjunction", "Meaning", "Effect on Word Order", "Example"],
["Coordinating", "und", "and", "Normal order", "Ich lerne und er schlaft."],
["Coordinating", "aber", "but", "Normal order", "Er ist nett, aber ich mag ihn nicht."],
["Coordinating", "oder", "or", "Normal order", "Kommst du oder bleibst du?"],
["Coordinating", "denn", "because/for", "Normal order", "Ich esse, denn ich habe Hunger."],
["Coordinating", "sondern", "but rather", "Normal order", "Er ist nicht krank, sondern mude."],
["Subordinating", "weil", "because", "VERB TO END", "Ich lerne, weil es wichtig ist."],
["Subordinating", "dass", "that", "VERB TO END", "Er sagt, dass er kommt."],
["Subordinating", "obwohl", "although", "VERB TO END", "Obwohl es regnet, gehe ich raus."],
["Subordinating", "wenn", "if/when", "VERB TO END", "Wenn ich Zeit habe, lese ich."],
["Subordinating", "als", "when (past)", "VERB TO END", "Als ich jung war, spielte ich."],
["Subordinating", "bevor", "before", "VERB TO END", "Bevor er geht, isst er."],
["Subordinating", "nachdem", "after", "VERB TO END", "Nachdem er gegessen hat, geht er."],
["Subordinating", "damit", "so that", "VERB TO END", "Ich lerne, damit ich bestehe."],
]
story.append(grammar_table(
conj_types[0], conj_types[1:],
col_widths=[32*mm, 26*mm, 24*mm, 36*mm, 72*mm]
))
story.append(PageBreak())
# ─── PAGE 5: CASES & PREPOSITIONS ────────────────────────────────────────────
story += section_banner("10. PREPOSITIONS BY CASE " + level_badge("A2") + "–" + level_badge("B1"), bg=C_A2)
prep_data = [
["Case", "Prepositions", "Example", "Tip"],
["ACCUSATIVE only", "durch, fur, gegen, ohne, um, bis, entlang",
"Das Geschenk ist fur dich.", "Motion TOWARD / purpose"],
["DATIVE only", "aus, bei, mit, nach, seit, von, zu, gegenuber",
"Ich fahre mit dem Bus.", "Location / point in time"],
["TWO-WAY (Acc/Dat)", "an, auf, hinter, in, neben, uber, unter, vor, zwischen",
"Ich gehe IN die Schule. (Acc)\nIch bin IN der Schule. (Dat)",
"WHERE TO? → Accusative\nWHERE? → Dative"],
["GENITIVE", "trotz, wegen, wahrend, statt",
"Wegen des Regens bleibe ich.", "Formal/written German"],
]
story.append(grammar_table(
prep_data[0], prep_data[1:],
col_widths=[36*mm, 62*mm, 60*mm, 32*mm]
))
story.append(Spacer(1,3))
story.append(tip_box("Two-way prepositions: WOHIN? (where to?) = Accusative movement. WO? (where?) = Dative location."))
story.append(Spacer(1,4))
# RELATIVE CLAUSES
story += section_banner("11. RELATIVE CLAUSES " + level_badge("B1"), bg=C_B1)
rel_data = [
["Gender/Case", "Nominative", "Accusative", "Dative"],
["Masculine", "der", "den", "dem"],
["Feminine", "die", "die", "der"],
["Neuter", "das", "das", "dem"],
["Plural", "die", "die", "denen"],
]
story.append(grammar_table(rel_data[0], rel_data[1:],
col_widths=[50*mm, 50*mm, 50*mm, 40*mm]))
story.append(Spacer(1,3))
rel_examples = [
["Example", "Translation"],
["Der Mann, DER hier wohnt, ist nett.", "The man WHO lives here is nice. (Masc. Nom.)"],
["Die Frau, DIE ich sehe, ist meine Chefin.", "The woman WHOM I see is my boss. (Fem. Acc.)"],
["Das Kind, DEM ich helfe, ist mein Neffe.", "The child WHOM I help is my nephew. (Neut. Dat.)"],
["Die Leute, DIE hier wohnen, sind freundlich.", "The people WHO live here are friendly. (Plural Nom.)"],
]
story.append(Spacer(1,2))
story.append(grammar_table(rel_examples[0], rel_examples[1:],
col_widths=[100*mm, 90*mm]))
story.append(Spacer(1,3))
story.append(tip_box("Relative clause verb goes to END. Match the pronoun (der/die/das/denen) to the GENDER of the noun, and CASE of its role in the clause."))
story.append(Spacer(1,4))
# REFLEXIVE VERBS
story += section_banner("12. REFLEXIVE VERBS " + level_badge("A2") + "–" + level_badge("B1"), bg=C_A2)
refl_data = [
["Pronoun", "Reflexive Pronoun (Acc)", "Example Verb: sich freuen"],
["ich", "mich", "Ich freue mich."],
["du", "dich", "Du freust dich."],
["er/sie/es", "sich", "Er freut sich."],
["wir", "uns", "Wir freuen uns."],
["ihr", "euch", "Ihr freut euch."],
["sie/Sie", "sich", "Sie freuen sich."],
]
cw5 = [40*mm, 60*mm, 90*mm]
story.append(grammar_table(refl_data[0], refl_data[1:], col_widths=cw5))
story.append(Spacer(1,3))
refl_verbs = [
["Common Reflexive Verb", "Meaning", "Common Reflexive Verb", "Meaning"],
["sich vorstellen", "to introduce oneself", "sich bewerben", "to apply (for job)"],
["sich freuen (auf)", "to look forward to", "sich befinden", "to be located"],
["sich interessieren (fur)", "to be interested in", "sich erinnern (an)", "to remember"],
["sich befassen (mit)", "to deal with", "sich melden", "to get in touch"],
]
cw6 = [52*mm, 42*mm, 52*mm, 44*mm]
story.append(grammar_table(refl_verbs[0], refl_verbs[1:], col_widths=cw6))
story.append(PageBreak())
# ─── PAGE 6: EXAM WRITING TEMPLATES ──────────────────────────────────────────
story += section_banner("13. GOETHE EXAM WRITING TEMPLATES " + level_badge("A1") + "–" + level_badge("B1"), bg=C_ACCENT)
story.append(Paragraph("<b>A1/A2 — Short Message / Form</b>", style_subsection))
msg_style = ParagraphStyle("MsgBox", fontSize=8.5, leading=12, textColor=C_BLACK,
fontName="Courier", leftIndent=8, spaceAfter=1)
box_data = [[Paragraph(line, msg_style)] for line in [
"Betreff: [Subject / Betreff]",
"",
"Liebe/r [Name],",
"",
"ich heisse [your name] und ich komme aus [country].",
"Ich wohne in [city] und ich arbeite als [job].",
"Ich [main sentence relevant to the task prompt].",
"",
"Viele Grusse,",
"[Your Name]",
]]
msg_table = Table(box_data, colWidths=[PAGE_W - 2*MARGIN])
msg_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), HexColor("#EBF5FB")),
("BOX", (0,0), (-1,-1), 1, C_A2),
("LEFTPADDING", (0,0), (-1,-1), 10),
("RIGHTPADDING", (0,0), (-1,-1), 10),
("TOPPADDING", (0,0), (-1,-1), 1),
("BOTTOMPADDING", (0,0), (-1,-1), 1),
]))
story.append(msg_table)
story.append(Spacer(1,5))
story.append(Paragraph("<b>B1 — Formal Email (Sehr geehrte...)</b>", style_subsection))
email_data = [[Paragraph(line, msg_style)] for line in [
"Betreff: [Topic]",
"",
"Sehr geehrte Damen und Herren, (or: Sehr geehrte/r Frau/Herr [Name],)",
"",
"ich schreibe Ihnen bezuglich [topic/reason for writing].",
"[Sentence 1: Explain situation or background].",
"[Sentence 2: State your request or problem clearly].",
"Ich wurde mich freuen, wenn Sie [your specific request].",
"",
"Fur eine Ruckmeldung bedanke ich mich im Voraus.",
"",
"Mit freundlichen Gruessen,",
"[Your Full Name]",
]]
email_table = Table(email_data, colWidths=[PAGE_W - 2*MARGIN])
email_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), HexColor("#F4ECF7")),
("BOX", (0,0), (-1,-1), 1, C_B1),
("LEFTPADDING", (0,0), (-1,-1), 10),
("RIGHTPADDING", (0,0), (-1,-1), 10),
("TOPPADDING", (0,0), (-1,-1), 1),
("BOTTOMPADDING", (0,0), (-1,-1), 1),
]))
story.append(email_table)
story.append(Spacer(1,5))
story.append(Paragraph("<b>B1 — Forum Post / Opinion (Forumsbeitrag)</b>", style_subsection))
forum_data = [[Paragraph(line, msg_style)] for line in [
"Meiner Meinung nach ist [topic] sehr wichtig / problematisch.",
"Einerseits [first argument or advantage].",
"Andererseits [counter-argument or disadvantage].",
"Ich finde, dass [your conclusion + reason with 'weil' or 'da'].",
"Deshalb bin ich der Ansicht, dass [final opinion / call to action].",
]]
forum_table = Table(forum_data, colWidths=[PAGE_W - 2*MARGIN])
forum_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), HexColor("#FEF9E7")),
("BOX", (0,0), (-1,-1), 1, C_YELLOW),
("LEFTPADDING", (0,0), (-1,-1), 10),
("RIGHTPADDING", (0,0), (-1,-1), 10),
("TOPPADDING", (0,0), (-1,-1), 1),
("BOTTOMPADDING", (0,0), (-1,-1), 1),
]))
story.append(forum_table)
story.append(Spacer(1,5))
# SPEAKING PHRASE BANK
story += section_banner("14. B1 SPEAKING — EXAM PHRASE BANK " + level_badge("B1"), bg=C_B1)
speak_data = [
["Function", "German Phrase", "English"],
["Opening presentation", "Ich mochte uber [Thema] sprechen.", "I would like to talk about [topic]."],
["Adding a point", "Ausserdem mochte ich erw\u00e4hnen, dass...", "Furthermore I would like to mention that..."],
["Giving opinion", "Meiner Meinung nach...", "In my opinion..."],
["Agreeing", "Da haben Sie recht. / Ich stimme zu.", "You are right. / I agree."],
["Disagreeing politely", "Das sehe ich etwas anders, weil...", "I see that slightly differently, because..."],
["Making suggestion", "Wie ware es, wenn wir...?", "How about if we...?"],
["Asking partner", "Was meinen Sie dazu?", "What do you think about that?"],
["Asking to repeat", "Konnten Sie das bitte wiederholen?", "Could you please repeat that?"],
["Buying time", "Das ist eine interessante Frage...", "That is an interesting question..."],
["Concluding", "Zusammenfassend kann ich sagen, dass...", "In summary I can say that..."],
["Transition", "Kommen wir jetzt zu... / Zum Schluss...", "Let's now move to... / In conclusion..."],
]
story.append(grammar_table(speak_data[0], speak_data[1:],
col_widths=[46*mm, 80*mm, 64*mm]))
story.append(PageBreak())
# ─── PAGE 7: VOCABULARY QUICK REFERENCE ──────────────────────────────────────
story += section_banner("15. HIGH-FREQUENCY VOCABULARY BY TOPIC " + level_badge("A1") + "–" + level_badge("B1"), bg=C_HEADER_BG)
story.append(Paragraph("<b>Numbers, Time & Dates</b>", style_subsection))
num_data = [
["German", "English", "German", "English", "German", "English"],
["eins (1)", "one", "elf (11)", "eleven", "hundert", "hundred"],
["zwei (2)", "two", "zwolf (12)", "twelve", "tausend", "thousand"],
["drei (3)", "three", "zwanzig (20)", "twenty", "Montag", "Monday"],
["vier (4)", "four", "dreissig (30)", "thirty", "Dienstag", "Tuesday"],
["funf (5)", "five", "vierzig (40)", "forty", "Mittwoch", "Wednesday"],
["sechs (6)", "six", "funfzig (50)", "fifty", "Donnerstag", "Thursday"],
["sieben (7)", "seven", "sechzig (60)", "sixty", "Freitag", "Friday"],
["acht (8)", "eight", "siebzig (70)", "seventy", "Samstag", "Saturday"],
["neun (9)", "nine", "achtzig (80)", "eighty", "Sonntag", "Sunday"],
["zehn (10)", "ten", "neunzig (90)", "ninety", "heute/morgen", "today/tomorrow"],
]
cw7 = [avail/6]*6
story.append(grammar_table(num_data[0], num_data[1:], col_widths=cw7))
story.append(Spacer(1,3))
story.append(Paragraph("<b>Essential Verbs A1–B1</b>", style_subsection))
verb_data = [
["Infinitive", "Meaning", "Infinitive", "Meaning", "Infinitive", "Meaning"],
["sein", "to be", "arbeiten", "to work", "sich freuen", "to be happy"],
["haben", "to have", "lernen", "to learn", "sich vorstellen", "to introduce oneself"],
["machen", "to do/make", "sprechen", "to speak", "sich befinden", "to be located"],
["gehen", "to go", "lesen", "to read", "sich bewerben", "to apply"],
["kommen", "to come", "schreiben", "to write", "wurden", "would (Konj. II)"],
["fahren", "to drive/travel", "horen", "to hear/listen", "wurde", "was made (passive)"],
["wohnen", "to live/reside", "verstehen", "to understand", "erklaren", "to explain"],
["heissen", "to be named", "helfen", "to help", "empfehlen", "to recommend"],
["kaufen", "to buy", "bezahlen", "to pay", "beschweren (sich)", "to complain"],
]
story.append(grammar_table(verb_data[0], verb_data[1:], col_widths=cw7))
story.append(Spacer(1,3))
story.append(Paragraph("<b>Opinion & Connective Words (B1 Writing/Speaking)</b>", style_subsection))
opinion_data = [
["German", "English", "German", "English"],
["Meiner Meinung nach", "In my opinion", "Deshalb / Deswegen", "Therefore / That's why"],
["Ich finde, dass...", "I think that...", "Ausserdem", "Furthermore / Besides"],
["Einerseits... andererseits...", "On one hand... on the other...", "Trotzdem", "Nevertheless"],
["Ich bin dafur / dagegen.", "I am for / against it.", "Im Gegensatz dazu", "In contrast to that"],
["Das stimmt (nicht).", "That is (not) true.", "Zum Beispiel (z.B.)", "For example"],
["Ich stimme zu / widerspreche.", "I agree / disagree.", "Im Allgemeinen", "In general"],
["Auf der einen Seite...", "On the one side...", "Zusammenfassend", "In summary"],
["Obwohl / Auch wenn", "Although / Even if", "Letztendlich", "Ultimately / In the end"],
]
story.append(grammar_table(opinion_data[0], opinion_data[1:],
col_widths=[60*mm, 45*mm, 55*mm, 30*mm]))
story.append(PageBreak())
# ─── PAGE 8: EXAM STRATEGY & QUICK REFERENCE ─────────────────────────────────
story += section_banner("16. GOETHE EXAM STRATEGY A1 / A2 / B1", bg=C_ACCENT)
strat_data = [
["Module", "Time", "Task Type", "Key Strategy"],
["Lesen (Reading)", "A1: 25min\nA2: 30min\nB1: 65min",
"Multiple choice, True/False, matching",
"Skim for gist first. Read questions BEFORE the text. Key words = answer clues."],
["Horen (Listening)", "A1: 20min\nA2: 30min\nB1: 40min",
"Multiple choice, gap fill, True/False",
"Read questions before audio plays. Write answers in note form first."],
["Schreiben (Writing)", "A1: 20min\nA2: 30min\nB1: 60min",
"A1/A2: form + short message\nB1: formal email + forum post",
"Use templates! Stay on topic. Check verb position in subordinate clauses."],
["Sprechen (Speaking)", "A1: 15min\nA2: 15min\nB1: 15min",
"A1: introduce self\nA2: negotiate\nB1: presentation + discussion",
"Speak clearly at natural pace. Use connectors. Ask partner questions. Don't memorize — react!"],
]
story.append(grammar_table(strat_data[0], strat_data[1:],
col_widths=[32*mm, 28*mm, 52*mm, 78*mm]))
story.append(Spacer(1,4))
story += section_banner("17. PASS MARK & SCORING", bg=C_MID_BLUE)
score_data = [
["Level", "Total Points", "Pass Mark", "Each Module Worth", "Can Fail One Module?"],
["A1", "100 pts", "60 pts (60%)", "25 pts each", "Yes — if total >= 60"],
["A2", "100 pts", "60 pts (60%)", "25 pts each", "Yes — if total >= 60"],
["B1", "100 pts", "60 pts (60%)", "25 pts each (modular)", "Yes — modules taken separately"],
]
story.append(grammar_table(score_data[0], score_data[1:],
col_widths=[24*mm, 30*mm, 32*mm, 42*mm, 62*mm]))
story.append(Spacer(1,4))
story += section_banner("18. 30-DAY STUDY PLAN SUMMARY", bg=C_HEADER_BG)
plan_data = [
["Week", "Focus", "Grammar Topics", "Daily Goal"],
["Week 1\n(Days 1–7)", "A1 Foundation", "Verbs, sein/haben, articles (Nom/Acc),\nquestion words, basic sentences",
"20 new words/day\n2 hrs study/day"],
["Week 2\n(Days 8–14)", "A2 Expansion", "Dative, modal verbs, Perfekt tense,\nseparable verbs, comparatives",
"25 new words/day\n2.5 hrs study/day"],
["Week 3\n(Days 15–21)", "B1 Bridge", "Prateritum, subordinate clauses,\nrelative clauses, Konjunktiv II, passive",
"30 new words/day\n3 hrs study/day"],
["Week 4\n(Days 22–30)", "Exam Simulation", "Full timed mock tests, writing templates,\nspeaking practice, weak point review",
"2 mock tests\n2.5 hrs study/day"],
]
story.append(grammar_table(plan_data[0], plan_data[1:],
col_widths=[25*mm, 30*mm, 85*mm, 50*mm]))
story.append(Spacer(1,3))
story.append(tip_box("You only need 60% to pass! Focus on consistency, not perfection. Speaking aloud every day — even 10 minutes — trains your brain faster than reading alone."))
story.append(Spacer(1,5))
# FOOTER NOTE
story.append(HRFlowable(width="100%", thickness=0.5, color=HexColor("#BDC3C7")))
story.append(Spacer(1,3))
story.append(Paragraph(
"Goethe-Zertifikat A1–B1 Grammar Cheat Sheet | Viel Erfolg bei deiner Prufung! | "
"Official Practice Tests: www.goethe.de/en/spr/kup/prf.html",
style_footer
))
# ─── BUILD ────────────────────────────────────────────────────────────────────
doc.build(story)
print(f"PDF created: {OUTPUT}")