Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions .github/quality-state.json

This file was deleted.

6 changes: 3 additions & 3 deletions docs/export-set-usage.de.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ python3 scripts/export_set.py <set-slug> [--lang <lang>] [--format yaml|json] [-
| `--lang` | Quellsprachen-Verzeichnis (`sets/<lang>/`), das einen Ordnernamen-Slug eindeutig macht, der unter mehreren Quellsprachen existiert | `de` |
| `--format` | Ausgabeformat: `yaml` oder `json` | `yaml` |
| `--out` | Pfad der Ausgabedatei (nicht kombinierbar mit `--split-size`) | `exports/<set-slug>-<lang>-<timestamp>.<format>` |
| `--split-size` | Export in mehrere Dateien von je hoechstens N Lektionen aufteilen, statt einer Datei | aus (eine Datei) |
| `--split-size` | Export in mehrere Dateien von je höchstens N Lektionen aufteilen, statt einer Datei | aus (eine Datei) |

Beispiele:

Expand All @@ -56,10 +56,10 @@ Das Verzeichnis `exports/` wird bei Bedarf angelegt und ist
**gitignored**: Exportdateien sind Wegwerf-Artefakte fürs Review und
werden nie committet.

Jeder von `--split-size` geschriebene Teil ist eigenstaendig: er traegt
Jeder von `--split-size` geschriebene Teil ist eigenständig: er trägt
seine eigene `review_instructions`-Kopie sowie die Felder
`part`/`of`/`lesson_count`/`total_lesson_count`, sodass jeder einzelne
Teil fuer sich, in beliebiger Reihenfolge, an eine KI zum Review
Teil für sich, in beliebiger Reihenfolge, an eine KI zum Review
gegeben werden kann.

Ein unbekannter oder mehrdeutiger Slug bricht mit Exit-Code 2 und
Expand Down
14 changes: 7 additions & 7 deletions manifest.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Wurzel-Manifest von alc-programming — die Programmier-Reihe.
# Wurzel-Manifest von alc-programming - die Programmier-Reihe.
#
# Adaptive-Learner-Content-Repo für Programmierung. Drei deutschsprachige
# Sets (domain: programming) nach aufsteigendem Niveau:
#
# python-basics Python — Grundlagen (A1, 15 Lektionen)
# python-basics Python - Grundlagen (A1, 15 Lektionen)
# react-grundlagen React-Grundlagen (Einsteiger) (A2, 5 Lektionen)
# react-fortgeschritten React Fortgeschritten (Teil 2) (B1, 8 Lektionen)
# react-19 React 19 in der Praxis (Teil 3) (B2, 8 Lektionen)
Expand All @@ -18,15 +18,15 @@
#
# Anleitung: docs/GETTING-STARTED.md
schema_version: '1.5'
name: Adaptive Learner — Programmierung
name: Adaptive Learner - Programmierung
description: >-
Programmier-Inhalte für Adaptive Learner: Python-Grundlagen für
Einsteiger sowie eine zweiteilige React-Reihe (Grundlagen und
Fortgeschritten), alle auf Deutsch erklärt.
sets:
- id: python-basics-from-de
title: Python — Grundlagen
title_native: Python — Basics
title: Python - Grundlagen
title_native: Python - Basics
target_language: de
source_language: de
domain: programming
Expand Down Expand Up @@ -79,8 +79,8 @@ sets:
lesson_count: 8
description: >-
Fortsetzung von „React-Grundlagen" (Teil 2, Fortgeschritten, B1): acht
Lektionen mit Schwerpunkt Multiple-Choice — Single-Answer als cloze
select-mode und „Wähle alle zutreffenden" als cloze multiselect — plus
Lektionen mit Schwerpunkt Multiple-Choice, Single-Answer als cloze
select-mode und „Wähle alle zutreffenden" als cloze multiselect, plus
matching und kurze Lückentext-Items. Themen: useEffect, Datenfetching,
useContext, useRef, Performance, Listen/Keys, Formulare, Komposition
und Patterns. React-18, Hooks-first, konzeptuell.
Expand Down
84 changes: 80 additions & 4 deletions scripts/check_prose.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,16 @@
Code is exempt from the umlaut check, and only from that one. An identifier is
spelled by whoever wrote it: "laeuft" as a variable name is a choice, "laeuft"
in a sentence is a misspelling. In a lesson file that means the code-bearing
fields (``passage``, ``sentence``, ``tokens``, ``code``) and the fenced blocks
inside a theory body; in Markdown it means the fenced blocks.
fields (``passage``, ``sentence``, ``tokens``, ``code``), a string ending in a
semicolon (a code line offered as an option) and the fenced blocks inside a
theory body; in Markdown it means the fenced blocks.

Machine values and typing variants are not prose either: the id fields of a
lesson file, the ``tags`` of a lesson or a YAML file, and every ``accept``
entry after the first. The app shows ``accept[0]`` as the solution; the later
entries only catch what a learner types, an ASCII spelling for a keyboard
without umlauts among them. A surname that is also a German word written
without its sharp s goes into ``PROPER_NAMES``, matched case-sensitively.

This file, its test, the stem generator and its data are exempt from the
umlaut check too, for the same reason the banned characters are built from
Expand Down Expand Up @@ -110,8 +118,40 @@ def _pair_decisions(stem: str, correction: str) -> dict[int, str]:
"neisser",
"jinaesseo",
"seonsaengnim",
# French "gros -> grosse", "Grosses bises" (adaptive-learner-content, fr-a2
# and en-b1, 2026-10-01). The cost: these two German misspellings of
# "große"/"großes" are no longer caught as whole words; "grossen",
# "grosser" and every compound still are.
"grosse",
"grosses",
}

# Surnames that are also a German word written without its sharp s. They are
# matched case-sensitively, so only the capitalised name is exempt and the
# lower-case word ("ich weiss", "gross") stays a finding. Found by:
# alc-psychology, "Hovland & Weiss" and James Gross's process model
# (2026-10-01).
PROPER_NAMES = {
"Weiss",
"Gross",
}

# Code syntax German prose never uses, in fields that are plain text and so
# carry code without backticks (a card's back, a hint): a subscript
# (d['key']), an increment (n++), a tag (<Name), a JSX expression ({name})
# and a method call (.get(key)).
CODE_SYNTAX = re.compile(
r"\[\s*['\"][^'\"\]]*['\"]\s*\]"
r"|\w+(?:\+\+|--)"
r"|</?[A-Za-z][\w.]*"
r"|\{[^{}\s]*\}"
r"|\.\w+\([^()]*\)"
)

# A YAML "tags" key, block or flow style. Its values are slugs, as in a lesson
# file's "tags" (MACHINE_KEYS).
YAML_TAGS = re.compile(r"^(\s*)(?:-\s+)?tags:\s*(.*)$")

# Fields of a lesson file that hold machine keys, not words: an id is looked up
# verbatim by the app and the manifest, so "correcting" its spelling silently
# renames the thing it names - and it is how a slug stops being ASCII without
Expand Down Expand Up @@ -227,13 +267,14 @@ def substituted_words(text: str) -> list[tuple[str, str]]:
umlauts in ``text``."""
findings = []
text = INLINE_CODE.sub(" ", text)
text = CODE_SYNTAX.sub(" ", text)
text = TECHNICAL_TOKEN.sub(
lambda match: " " if match.group(0) == match.group(0).lower() else match.group(0).replace("_", " "),
text,
)
for word in WORD.findall(text):
lowered = word.lower()
if lowered in FOREIGN_LOOKALIKES or _is_identifier(word):
if lowered in FOREIGN_LOOKALIKES or word in PROPER_NAMES or _is_identifier(word):
continue
if lowered in WHOLE_WORDS:
suggestion = WHOLE_WORDS[lowered]
Expand Down Expand Up @@ -289,6 +330,8 @@ def prose_segments(path: str, text: str) -> list[tuple[int, str]]:
line_no = next((i for i, line in enumerate(lines, start=1) if head[:60] in line), 0)
segments.append((line_no, _outside_fences(value)))
return segments
if path.endswith((".yaml", ".yml")):
return _yaml_segments(text)
if path.endswith(".md"):
segments = []
in_fence = False
Expand All @@ -302,18 +345,51 @@ def prose_segments(path: str, text: str) -> list[tuple[int, str]]:
return list(enumerate(text.splitlines(), start=1))


def _yaml_segments(text: str) -> list[tuple[int, str]]:
"""(line number, prose) for a YAML file: every line except the values of a
``tags`` key. A comment inside a tags block is prose."""
segments = []
tags_indent = None
for number, line in enumerate(text.splitlines(), start=1):
stripped = line.lstrip()
if tags_indent is not None:
if not stripped or stripped.startswith("#"):
segments.append((number, line))
continue
if stripped.startswith("-") and len(line) - len(stripped) >= tags_indent:
continue
tags_indent = None
match = YAML_TAGS.match(line)
if match is None:
segments.append((number, line))
continue
value = match.group(2)
if not value or value.startswith("#"):
tags_indent = len(match.group(1))
segments.append((number, value))
return segments


def _walk_json(node, in_code: bool, out: list[str]) -> None:
if isinstance(node, dict):
# An inline example is prose when it is a sample sentence and code when
# it declares a language; the field name alone cannot tell them apart.
example_is_code = bool(node.get("language")) and "content" in node
for key, value in node.items():
child_is_code = in_code or key in CODE_KEYS or (example_is_code and key == "content")
if key == "accept" and isinstance(value, list):
# The app shows accept[0] as the solution; the later entries
# only catch what a learner types, an ASCII spelling for a
# keyboard without umlauts among them.
_walk_json(value[:1], child_is_code, out)
continue
_walk_json(value, child_is_code, out)
elif isinstance(node, list):
for value in node:
_walk_json(value, in_code, out)
elif isinstance(node, str) and not in_code:
elif isinstance(node, str) and not in_code and not node.rstrip().endswith(";"):
# A string ending in ";" is a code statement (a select cloze offering
# code lines); prose does not end a field with a semicolon.
out.append(node)


Expand Down
18 changes: 9 additions & 9 deletions sets/de/python-basics/lessons/01-erste-schritte.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "01-erste-schritte",
"title": "Erste Schritte mit Python",
"description": "Was Python ist, dein erstes Programm, print(), Kommentare und typische Anfängerfehler — auf Deutsch erklärt.",
"description": "Was Python ist, dein erstes Programm, print(), Kommentare und typische Anfängerfehler, auf Deutsch erklärt.",
"target_language": "de",
"source_language": "de",
"domain": "programming",
Expand Down Expand Up @@ -73,7 +73,7 @@
"id": "py-string",
"stable_id": "card-ms942h7d006vw",
"front": "String (str)",
"back": "Eine Zeichenkette — Text, der in Anführungszeichen steht.",
"back": "Eine Zeichenkette, Text, der in Anführungszeichen steht.",
"code_snippet": "name = 'Anna'\nprint(name)",
"code_language": "python",
"expected_output": "Anna",
Expand All @@ -87,7 +87,7 @@
"id": "py-comment",
"stable_id": "card-ms942h7d007vc",
"front": "Kommentar (#)",
"back": "Text nach einem # wird vom Interpreter ignoriert — er dient nur dem Menschen.",
"back": "Text nach einem # wird vom Interpreter ignoriert, er dient nur dem Menschen.",
"code_snippet": "# Das ist ein Kommentar\nprint('Hi')",
"code_language": "python",
"expected_output": "Hi",
Expand All @@ -101,7 +101,7 @@
"id": "py-quotes",
"stable_id": "card-ms942h7d008h1",
"front": "Anführungszeichen",
"back": "Strings stehen in einfachen ' ' oder doppelten \" \" Anführungszeichen — Hauptsache, beide Seiten sind gleich.",
"back": "Strings stehen in einfachen ' ' oder doppelten \" \" Anführungszeichen, Hauptsache, beide Seiten sind gleich.",
"code_snippet": "print(\"Tschüss\")",
"code_language": "python",
"expected_output": "Tschüss",
Expand All @@ -116,7 +116,7 @@
"stable_id": "card-ms942h7d0098l",
"front": "SyntaxError",
"back": "Fehlermeldung, wenn der Code grammatikalisch falsch ist, z. B. bei einer fehlenden Klammer.",
"notes": "Python zeigt die Zeile mit dem Problem an — sehr hilfreich beim Suchen.",
"notes": "Python zeigt die Zeile mit dem Problem an, sehr hilfreich beim Suchen.",
"media_type": "text",
"difficulty": 2,
"tags": ["fehler"]
Expand All @@ -136,20 +136,20 @@
{
"id": "intro",
"type": "theory",
"title": "Was ist Python — und dein erstes Programm",
"body": "# Erste Schritte mit Python\n\n**Python** ist eine **Programmiersprache**: eine Sprache, in der wir einem Computer Anweisungen geben. Python gilt als besonders gut lesbar und einsteigerfreundlich — der Code sieht fast wie englischer Text aus.\n\nDas Programm, das deinen Code ausführt, heißt **Interpreter**. Er liest jede Zeile und führt sie sofort aus.\n\nDas klassische erste Programm gibt einen Gruß aus:\n\n```python\nprint('Hallo Welt')\n```\n\nAusgabe:\n\n```\nHallo Welt\n```\n\nHerzlichen Glückwunsch — das ist ein vollständiges Python-Programm."
"title": "Was ist Python - und dein erstes Programm",
"body": "# Erste Schritte mit Python\n\n**Python** ist eine **Programmiersprache**: eine Sprache, in der wir einem Computer Anweisungen geben. Python gilt als besonders gut lesbar und einsteigerfreundlich, der Code sieht fast wie englischer Text aus.\n\nDas Programm, das deinen Code ausführt, heißt **Interpreter**. Er liest jede Zeile und führt sie sofort aus.\n\nDas klassische erste Programm gibt einen Gruß aus:\n\n```python\nprint('Hallo Welt')\n```\n\nAusgabe:\n\n```\nHallo Welt\n```\n\nHerzlichen Glückwunsch, das ist ein vollständiges Python-Programm."
},
{
"id": "step-print",
"type": "theory",
"title": "Text ausgeben mit print()",
"body": "## Die Funktion print()\n\n**print()** ist die wichtigste Funktion am Anfang: Sie gibt etwas auf der **Konsole** aus.\n\n- Der auszugebende Text steht in **Anführungszeichen** — einfach `' '` oder doppelt `\" \"`.\n- Mehrere Werte werden durch **Kommas** getrennt; Python setzt automatisch ein Leerzeichen dazwischen:\n\n```python\nprint('Hallo', 'Welt')\n```\n\nAusgabe:\n\n```\nHallo Welt\n```\n\nText in Anführungszeichen nennt man einen **String** (`str`)."
"body": "## Die Funktion print()\n\n**print()** ist die wichtigste Funktion am Anfang: Sie gibt etwas auf der **Konsole** aus.\n\n- Der auszugebende Text steht in **Anführungszeichen**, einfach `' '` oder doppelt `\" \"`.\n- Mehrere Werte werden durch **Kommas** getrennt; Python setzt automatisch ein Leerzeichen dazwischen:\n\n```python\nprint('Hallo', 'Welt')\n```\n\nAusgabe:\n\n```\nHallo Welt\n```\n\nText in Anführungszeichen nennt man einen **String** (`str`)."
},
{
"id": "step-fehler",
"type": "theory",
"title": "Kommentare und typische Fehler",
"body": "## Kommentare\n\nMit dem Zeichen **#** schreibt man einen **Kommentar**. Alles nach `#` wird vom Interpreter ignoriert — Kommentare sind nur für Menschen da:\n\n```python\n# Das hier wird ignoriert\nprint('Hi') # auch das nach dem Code\n```\n\n## Zwei häufige Anfängerfehler\n\n- **SyntaxError** — der Code ist grammatikalisch falsch, z. B. eine **Klammer** oder ein **Anführungszeichen** fehlt.\n- **Einrückung (Indentation)** — Python nutzt Leerzeichen am Zeilenanfang, um Blöcke zu strukturieren. In dieser Lektion reicht es zu wissen, dass Einrückung in Python eine Bedeutung hat."
"body": "## Kommentare\n\nMit dem Zeichen **#** schreibt man einen **Kommentar**. Alles nach `#` wird vom Interpreter ignoriert, Kommentare sind nur für Menschen da:\n\n```python\n# Das hier wird ignoriert\nprint('Hi') # auch das nach dem Code\n```\n\n## Zwei häufige Anfängerfehler\n\n- **SyntaxError**: der Code ist grammatikalisch falsch, z. B. eine **Klammer** oder ein **Anführungszeichen** fehlt.\n- **Einrückung (Indentation)**: Python nutzt Leerzeichen am Zeilenanfang, um Blöcke zu strukturieren. In dieser Lektion reicht es zu wissen, dass Einrückung in Python eine Bedeutung hat."
},
{
"id": "ex-cloze-print",
Expand Down
10 changes: 5 additions & 5 deletions sets/de/python-basics/lessons/02-variablen-und-datentypen.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "02-variablen-und-datentypen",
"title": "Variablen und Datentypen",
"description": "Variablen anlegen und überschreiben, die Grundtypen int, float, str und bool, die Funktion type() und Namensregeln — auf Deutsch erklärt.",
"description": "Variablen anlegen und überschreiben, die Grundtypen int, float, str und bool, die Funktion type() und Namensregeln, auf Deutsch erklärt.",
"target_language": "de",
"source_language": "de",
"domain": "programming",
Expand All @@ -12,7 +12,7 @@
"stable_id": "card-ms942h7f00inq",
"front": "Variable",
"back": "Ein benannter Speicherplatz, der einen Wert enthält. Mit = wird ihr ein Wert zugewiesen.",
"notes": "Der Name einer Variablen ist frei wählbar — solange er den Namensregeln folgt.",
"notes": "Der Name einer Variablen ist frei wählbar, solange er den Namensregeln folgt.",
"media_type": "text",
"difficulty": 1,
"tags": ["grundlagen", "variablen"]
Expand Down Expand Up @@ -147,7 +147,7 @@
"id": "intro-variablen",
"type": "theory",
"title": "Variablen und Zuweisung",
"body": "# Variablen und Datentypen\n\nEine **Variable** ist ein benannter Speicherplatz. Mit dem **Zuweisungsoperator `=`** speichert man einen Wert unter einem Namen:\n\n```python\nname = 'Anna'\nalter = 25\n```\n\nDanach kann man den Namen überall im Code verwenden:\n\n```python\nprint(name)\nprint(alter)\n```\n\nAusgabe:\n\n```\nAnna\n25\n```\n\nMan kann einer Variablen jederzeit einen **neuen Wert zuweisen** — der alte wird dann ersetzt:\n\n```python\nx = 10\nx = 20\nprint(x) # gibt 20 aus\n```"
"body": "# Variablen und Datentypen\n\nEine **Variable** ist ein benannter Speicherplatz. Mit dem **Zuweisungsoperator `=`** speichert man einen Wert unter einem Namen:\n\n```python\nname = 'Anna'\nalter = 25\n```\n\nDanach kann man den Namen überall im Code verwenden:\n\n```python\nprint(name)\nprint(alter)\n```\n\nAusgabe:\n\n```\nAnna\n25\n```\n\nMan kann einer Variablen jederzeit einen **neuen Wert zuweisen**, der alte wird dann ersetzt:\n\n```python\nx = 10\nx = 20\nprint(x) # gibt 20 aus\n```"
},
{
"id": "step-datentypen",
Expand All @@ -159,7 +159,7 @@
"id": "step-namensregeln",
"type": "theory",
"title": "Namensregeln und Snake Case",
"body": "## Gültige Variablennamen\n\nFür Variablennamen gilt:\n\n- Nur **Buchstaben**, **Ziffern** und **Unterstrich** `_`.\n- **Kein Leerzeichen** — das würde Python verwirren.\n- Nicht mit einer **Ziffer beginnen**: `1name` ist ungültig, `name1` ist erlaubt.\n- Python unterscheidet **Groß- und Kleinschreibung**: `Alter` und `alter` sind zwei verschiedene Variablen.\n\nDer Python-Stilguide empfiehlt **Snake Case** für mehrteilige Namen:\n\n```python\nhoechste_punktzahl = 100\nmein_name = 'Lena'\n```\n\nUnzulässige Namen (verursachen einen `SyntaxError`):\n\n```python\n# 2wert = 5 → beginnt mit Ziffer\n# mein name = 5 → enthält Leerzeichen\n```"
"body": "## Gültige Variablennamen\n\nFür Variablennamen gilt:\n\n- Nur **Buchstaben**, **Ziffern** und **Unterstrich** `_`.\n- **Kein Leerzeichen**: das würde Python verwirren.\n- Nicht mit einer **Ziffer beginnen**: `1name` ist ungültig, `name1` ist erlaubt.\n- Python unterscheidet **Groß- und Kleinschreibung**: `Alter` und `alter` sind zwei verschiedene Variablen.\n\nDer Python-Stilguide empfiehlt **Snake Case** für mehrteilige Namen:\n\n```python\nhoechste_punktzahl = 100\nmein_name = 'Lena'\n```\n\nUnzulässige Namen (verursachen einen `SyntaxError`):\n\n```python\n# 2wert = 5 → beginnt mit Ziffer\n# mein name = 5 → enthält Leerzeichen\n```"
},
{
"id": "ex-cloze-zuweisung",
Expand All @@ -177,7 +177,7 @@
{ "accept": ["="] }
],
"cloze_mode": "type",
"hint": "Die Zuweisung — nicht der doppelte Vergleichsoperator =="
"hint": "Die Zuweisung, nicht der doppelte Vergleichsoperator =="
}
},
{
Expand Down
Loading
Loading