From bb93ec76ece4c457951c7b74b8fd85d495cc99d3 Mon Sep 17 00:00:00 2001 From: pepaskopec-blip Date: Wed, 7 Oct 2026 10:42:28 +0200 Subject: [PATCH] Add four years of English from the gymnasium syllabus. Each year mixes listening, reading, writing, gap-fill, quizzes, and a year test so the subject is usable through the maturita. Co-authored-by: Cursor --- README.md | 12 +- .../org/maturita/maturita/AppViewModel.kt | 5 + .../org/maturita/maturita/MainActivity.kt | 17 + .../main/java/org/maturita/maturita/Route.kt | 20 +- .../org/maturita/maturita/data/Content.kt | 11 + .../maturita/maturita/data/ProgressStore.kt | 13 + .../org/maturita/maturita/ui/Exercises.kt | 159 ++ .../java/org/maturita/maturita/ui/Icons.kt | 20 + .../java/org/maturita/maturita/ui/Screens.kt | 29 + data/changelog.txt | 6 + ios/Graduately/AppModel.swift | 5 + ios/Graduately/Data/Content.swift | 17 + ios/Graduately/Data/ProgressStore.swift | 13 + ios/Graduately/Route.swift | 26 + ios/Graduately/UI/Components.swift | 1 + ios/Graduately/UI/ExercisesMore.swift | 186 ++ ios/Graduately/UI/Icons.swift | 24 + ios/Graduately/UI/Screens.swift | 68 + scripts/extract-android-content.py | 64 +- scripts/gen-english.py | 2537 +++++++++++++++++ src/english.c | 1331 +++++++++ src/english_i18n.inc | 86 + src/english_lessons.inc | 1907 +++++++++++++ src/graduately.h | 10 + src/i18n.c | 5 +- src/main.c | 4 + src/progress.c | 1 + src/search.c | 20 +- src/stats.c | 7 +- src/subjects.c | 13 +- src/theme.c | 1 + src/util.c | 3 + 32 files changed, 6607 insertions(+), 14 deletions(-) create mode 100644 scripts/gen-english.py create mode 100644 src/english.c create mode 100644 src/english_i18n.inc create mode 100644 src/english_lessons.inc diff --git a/README.md b/README.md index c7a9d21..a0ea6c1 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ Vzdělávací aplikace k přípravě na maturitu (repo `Graduately`). / An educa ## Česky -Rozhraní je v češtině a angličtině. Hotových je pět předmětů; ostatní jsou -zatím zamčené placeholdery. Aplikace se po instalaci aktualizuje sama. +Rozhraní je v češtině a angličtině. Otevřené předměty jsou v seznamu níže; +ostatní jsou zatím zamčené. Aplikace se po instalaci aktualizuje sama. | Úvod | Předměty | Německá cesta | | ---- | -------- | ------------- | @@ -78,6 +78,8 @@ Intel Mac, jiná architektura nebo úpravy kódu → [sestavení ze zdroje](#ses - **Občanská nauka** — 1. ročník, 17 lekcí (člověk v lidském společenství); 2. ročník, 16 lekcí (člověk jako občan); 3. ročník, 16 lekcí (člověk a právo); 4. ročník, 16 lekcí (hospodářství, svět a filozofie) +- **Angličtina** — 4 ročníky, 32 lekcí podle osnov gymnázia (A2 až maturita): + poslech, čtení, psaní, doplňování, kvízy a ročníkové testy - statistiky, hledání (lupa nebo `Cmd/Ctrl+K`), 10 témat, tmavý/světlý režim, čeština/angličtina - ukončení: `Cmd/Super+Q` nebo `Alt+F4` @@ -210,8 +212,8 @@ Licence: [GPL-3.0](LICENSE). ## English -The UI is Czech and English. Five subjects are playable; the rest are locked -placeholders. After install the app updates itself. +The UI is Czech and English. Open subjects are listed below; the rest are +still locked. After install the app updates itself. | Welcome | Subjects | German path | | ------- | -------- | ----------- | @@ -282,6 +284,8 @@ Intel Mac, another architecture, or hacking on the code → year 2, 16 lessons (the citizen in a democratic state); year 3, 16 lessons (the individual and the law); year 4, 16 lessons (the economy, the world and philosophy) +- **English** — 4 years, 32 lessons from the gymnasium syllabus (A2 through + the maturita): listening, reading, writing, gap-fill, quizzes and year tests - statistics, search (magnifier or `Cmd/Ctrl+K`), 10 palettes, dark/light mode, Czech/English - quit with `Cmd/Super+Q` or `Alt+F4` diff --git a/android/app/src/main/java/org/maturita/maturita/AppViewModel.kt b/android/app/src/main/java/org/maturita/maturita/AppViewModel.kt index 4792ee8..7eb1273 100644 --- a/android/app/src/main/java/org/maturita/maturita/AppViewModel.kt +++ b/android/app/src/main/java/org/maturita/maturita/AppViewModel.kt @@ -146,6 +146,11 @@ class AppViewModel(app: Application) : AndroidViewModel(app) { refresh() } + fun markEn(year: Int, id: Int) { + progress.markEn(year, id) + refresh() + } + fun markLit(id: Int) { progress.markLit(id) refresh() diff --git a/android/app/src/main/java/org/maturita/maturita/MainActivity.kt b/android/app/src/main/java/org/maturita/maturita/MainActivity.kt index 5d61e24..8133702 100644 --- a/android/app/src/main/java/org/maturita/maturita/MainActivity.kt +++ b/android/app/src/main/java/org/maturita/maturita/MainActivity.kt @@ -49,6 +49,8 @@ import org.maturita.maturita.ui.On3QuizScreen import org.maturita.maturita.ui.On4QuizScreen import org.maturita.maturita.ui.OnQuizScreen import org.maturita.maturita.ui.OnYearsScreen +import org.maturita.maturita.ui.EnYearsScreen +import org.maturita.maturita.ui.EngExScreen import org.maturita.maturita.ui.PlotScreen import org.maturita.maturita.ui.RoadmapScreen import org.maturita.maturita.ui.SearchScreen @@ -238,6 +240,21 @@ private fun MaturitaAppBody(vm: AppViewModel) { } } is Route.HwEx -> HwQuizScreen(vm, r.id) + Route.EnYears -> EnYearsScreen(vm) + is Route.EnMap -> LessonListScreen( + vm, vm.tr("en_year${r.year}"), vm.tr("en_y${r.year}_sub"), + vm.content.enYear(r.year).map { l -> + val id = l.int("id") + LessonRow(vm.tr(l.str("titleKey")), vm.progress.enDone(r.year, id), Route.EnLesson(r.year, id)) + }, + ) + is Route.EnLesson -> { + val l = vm.content.enLesson(r.year, r.id) + if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) { + vm.replace(Route.EnEx(r.year, r.id)) + } + } + is Route.EnEx -> EngExScreen(vm, r.year, r.id) Route.OnYears -> OnYearsScreen(vm) Route.OnMap -> LessonListScreen( vm, vm.tr("on_year1"), vm.tr("on_sub"), diff --git a/android/app/src/main/java/org/maturita/maturita/Route.kt b/android/app/src/main/java/org/maturita/maturita/Route.kt index 8f1a030..a25370b 100644 --- a/android/app/src/main/java/org/maturita/maturita/Route.kt +++ b/android/app/src/main/java/org/maturita/maturita/Route.kt @@ -30,6 +30,10 @@ sealed class Route(val page: String) { data object On4Map : Route("on4map") data class On4Lesson(val id: Int) : Route("on4unit$id") data class On4Ex(val id: Int) : Route("on4ex$id") + data object EnYears : Route("enyears") + data class EnMap(val year: Int) : Route("en${year}map") + data class EnLesson(val year: Int, val id: Int) : Route("en${year}unit$id") + data class EnEx(val year: Int, val id: Int) : Route("en${year}ex$id") data object LitYears : Route("lityears") data object LitMap : Route("litmap") data class LitLesson(val id: Int) : Route("litunit$id") @@ -93,7 +97,7 @@ sealed class Route(val page: String) { /** Screens with their own bottom action bar; the navigation bar stays hidden there. */ val isExercise: Boolean get() = this is GermanEx || this is Vocab || this is NetEx || this is HwEx || - this is OnEx || this is On2Ex || this is On3Ex || this is On4Ex || this is MluvEx || this is BookQuiz || this is BookPlot || + this is OnEx || this is On2Ex || this is On3Ex || this is On4Ex || this is EnLesson || this is EnEx || this is MluvEx || this is BookQuiz || this is BookPlot || this is NetLesson || this is HwLesson || this is OnLesson || this is On2Lesson || this is On3Lesson || this is On4Lesson || this is LitLesson || this is LitEx || this is Lit2Lesson || this is Lit2Ex || this is Lit3Lesson || this is Lit3Ex || this is Lit4Lesson || this is Lit4Ex || @@ -134,6 +138,10 @@ sealed class Route(val page: String) { On4Map -> listOf(Subjects, OnYears, On4Map) is On4Lesson -> listOf(Subjects, OnYears, On4Map, this) is On4Ex -> listOf(Subjects, OnYears, On4Map, On4Lesson(id), this) + EnYears -> listOf(Subjects, EnYears) + is EnMap -> listOf(Subjects, EnYears, this) + is EnLesson -> listOf(Subjects, EnYears, EnMap(year), this) + is EnEx -> listOf(Subjects, EnYears, EnMap(year), EnLesson(year, id), this) LitYears -> listOf(Subjects, CzechMap, LitYears) LitMap -> listOf(Subjects, CzechMap, LitYears, LitMap) is LitLesson -> listOf(Subjects, CzechMap, LitYears, LitMap, this) @@ -208,6 +216,7 @@ fun routeFromPage(page: String): Route? = when (page) { "on2map" -> Route.On2Map "on3map" -> Route.On3Map "on4map" -> Route.On4Map + "enyears" -> Route.EnYears "lityears" -> Route.LitYears "litmap" -> Route.LitMap "lit2map" -> Route.Lit2Map @@ -348,6 +357,15 @@ fun routeFromPage(page: String): Route? = when (page) { Regex("""litex(\d+)""").matchEntire(page)?.let { return Route.LitEx(it.groupValues[1].toInt()) } + Regex("""en(\d)map""").matchEntire(page)?.let { + return Route.EnMap(it.groupValues[1].toInt()) + } + Regex("""en(\d)unit(\d+)""").matchEntire(page)?.let { + return Route.EnLesson(it.groupValues[1].toInt(), it.groupValues[2].toInt()) + } + Regex("""en(\d)ex(\d+)""").matchEntire(page)?.let { + return Route.EnEx(it.groupValues[1].toInt(), it.groupValues[2].toInt()) + } Regex("""on4unit(\d+)""").matchEntire(page)?.let { return Route.On4Lesson(it.groupValues[1].toInt()) } diff --git a/android/app/src/main/java/org/maturita/maturita/data/Content.kt b/android/app/src/main/java/org/maturita/maturita/data/Content.kt index b28c5b4..6ffbe6c 100644 --- a/android/app/src/main/java/org/maturita/maturita/data/Content.kt +++ b/android/app/src/main/java/org/maturita/maturita/data/Content.kt @@ -33,6 +33,10 @@ class Content(root: JSONObject) { val on2 = raw.arr("on2") val on3 = raw.arr("on3") val on4 = raw.arr("on4") + val en = raw.arr("en") + val en2 = raw.arr("en2") + val en3 = raw.arr("en3") + val en4 = raw.arr("en4") val lit = raw.arr("lit") val lit2 = raw.arr("lit2") val lit3 = raw.arr("lit3") @@ -59,6 +63,13 @@ class Content(root: JSONObject) { fun on2Lesson(id: Int) = on2.firstOrNull { it.int("id") == id } fun on3Lesson(id: Int) = on3.firstOrNull { it.int("id") == id } fun on4Lesson(id: Int) = on4.firstOrNull { it.int("id") == id } + fun enYear(year: Int) = when (year) { + 2 -> en2 + 3 -> en3 + 4 -> en4 + else -> en + } + fun enLesson(year: Int, id: Int) = enYear(year).firstOrNull { it.int("id") == id } fun litLesson(id: Int) = lit.firstOrNull { it.int("id") == id } fun lit2Lesson(id: Int) = lit2.firstOrNull { it.int("id") == id } fun lit3Lesson(id: Int) = lit3.firstOrNull { it.int("id") == id } diff --git a/android/app/src/main/java/org/maturita/maturita/data/ProgressStore.kt b/android/app/src/main/java/org/maturita/maturita/data/ProgressStore.kt index 334094b..e546fdc 100644 --- a/android/app/src/main/java/org/maturita/maturita/data/ProgressStore.kt +++ b/android/app/src/main/java/org/maturita/maturita/data/ProgressStore.kt @@ -44,6 +44,9 @@ class ProgressStore(context: Context) { fun on4Done(id: Int) = prefs.getBoolean("on4.$id", false) fun markOn4(id: Int) { prefs.edit().putBoolean("on4.$id", true).apply() } + fun enDone(year: Int, id: Int) = prefs.getBoolean("en.$year.$id", false) + fun markEn(year: Int, id: Int) { prefs.edit().putBoolean("en.$year.$id", true).apply() } + fun litDone(id: Int) = prefs.getBoolean("lit.$id", false) fun markLit(id: Int) { prefs.edit().putBoolean("lit.$id", true).apply() } fun lit2Done(id: Int) = prefs.getBoolean("lit2.$id", false) @@ -146,6 +149,16 @@ fun summarize(content: Content, p: ProgressStore): ProgressSum { sum.doneUnits += 1 } } + for (year in 1..4) { + content.enYear(year).forEach { l -> + sum.totalEx += 1 + sum.openUnits += 1 + if (p.enDone(year, l.int("id"))) { + sum.doneEx += 1 + sum.doneUnits += 1 + } + } + } content.lit.forEach { l -> sum.totalEx += 1 sum.openUnits += 1 diff --git a/android/app/src/main/java/org/maturita/maturita/ui/Exercises.kt b/android/app/src/main/java/org/maturita/maturita/ui/Exercises.kt index 93d3010..2d732c4 100644 --- a/android/app/src/main/java/org/maturita/maturita/ui/Exercises.kt +++ b/android/app/src/main/java/org/maturita/maturita/ui/Exercises.kt @@ -38,8 +38,15 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text +import android.os.Handler +import android.os.Looper +import android.speech.tts.TextToSpeech +import android.speech.tts.UtteranceProgressListener import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.SideEffect +import androidx.compose.ui.platform.LocalContext +import java.util.Locale import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateListOf @@ -1370,4 +1377,156 @@ fun PlotScreen(vm: AppViewModel, id: String) { put("meanings", book.o.optJSONArray("plotMeaning")) }) DispatchExercise(vm, spec, vm.tr(book.str("plotTitle"))) { vm.markBookPlot(id) } +} + +@Composable +fun EngExScreen(vm: AppViewModel, year: Int, id: Int) { + val lesson = vm.content.enLesson(year, id) ?: return + val readQ = lesson.arr("readQuiz") + val listenQ = lesson.arr("listenQuiz") + val quiz = lesson.arr("quiz") + val gapRows = lesson.arr("gaps") + val writing = lesson.obj("writing") + val listening = lesson.strOrNull("listening") + val readPicks = remember(year, id) { mutableStateListOf(*Array(readQ.size) { -1 }) } + val listenPicks = remember(year, id) { mutableStateListOf(*Array(listenQ.size) { -1 }) } + val quizPicks = remember(year, id) { mutableStateListOf(*Array(quiz.size) { -1 }) } + val gapText = remember(year, id) { mutableStateListOf(*Array(gapRows.size) { "" }) } + var essay by remember(year, id) { mutableStateOf("") } + var played by remember(year, id) { mutableStateOf(false) } + var showScript by remember(year, id) { mutableStateOf(false) } + var revealed by remember(year, id) { mutableStateOf(false) } + var showModel by remember(year, id) { mutableStateOf(false) } + var fb by remember(year, id) { mutableStateOf("" to "") } + val context = LocalContext.current + var tts by remember { mutableStateOf(null) } + DisposableEffect(context) { + var engine: TextToSpeech? = null + engine = TextToSpeech(context) { status -> + if (status == TextToSpeech.SUCCESS) engine?.language = Locale.UK + } + tts = engine + onDispose { + engine?.stop() + engine?.shutdown() + } + } + + fun picksOk(questions: List, picks: List) = + questions.indices.all { picks[it] == questions[it].int("correct") } + + ExerciseScaffold(vm, vm.tr(lesson.str("titleKey")), null, vm.tr("check"), { + if (listening != null && !played) { + fb = vm.tr("en_listen_first") to "err" + } else { + val readOk = picksOk(readQ, readPicks) + val listenOk = picksOk(listenQ, listenPicks) + val quizOk = picksOk(quiz, quizPicks) + val gapOk = gapRows.indices.count { + answerAccepts(normalizeAnswer(gapText[it]), gapRows[it].str("answers")) + } + val writeOk = if (writing == null) { + true + } else { + val norm = " ${normalizeAnswer(essay)} " + val keysOk = writing.str("keys").split("|").all { key -> + val w = normalizeAnswer(key) + w.isEmpty() || norm.contains(" $w ") + } + keysOk && normalizeAnswer(essay).split(" ").count { it.isNotEmpty() } >= writing.int("minWords", 20) + } + revealed = true + val all = readOk && listenOk && quizOk && gapOk == gapRows.size && writeOk + fb = when { + all -> { + showModel = true + vm.markEn(year, id) + vm.tr("feedback_ok") to "ok" + } + writing != null && !writeOk && readOk && listenOk && quizOk && gapOk == gapRows.size -> + vm.tr("en_write_short") to "err" + else -> vm.tr("feedback_retry_short") to "err" + } + } + }, fb.first, fb.second) { + lesson.strOrNull("reading")?.let { reading -> + Text(vm.tr("en_sec_read"), style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(top = 8.dp)) + Text(reading) + EngChoices(readQ, readPicks, revealed) { index, option -> readPicks[index] = option } + } + if (listening != null) { + Text(vm.tr("en_sec_listen"), style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(top = 8.dp)) + Text(vm.tr("en_listen_note"), color = MaterialTheme.colorScheme.onSurfaceVariant) + OutlinedButton(onClick = { + played = true + showScript = false + val engine = tts + if (engine == null) { + showScript = true + } else { + engine.language = Locale.UK + engine.setOnUtteranceProgressListener(object : UtteranceProgressListener() { + override fun onStart(utteranceId: String?) {} + override fun onDone(utteranceId: String?) { + Handler(Looper.getMainLooper()).post { showScript = true } + } + @Deprecated("Deprecated in Java") + override fun onError(utteranceId: String?) { + Handler(Looper.getMainLooper()).post { showScript = true } + } + }) + engine.speak(listening, TextToSpeech.QUEUE_FLUSH, null, "en-lesson") + val wait = (listening.split(" ").size * 420L).coerceAtLeast(2500L) + Handler(Looper.getMainLooper()).postDelayed({ showScript = true }, wait) + } + }) { Text(vm.tr("en_play")) } + if (showScript) Text(listening) + EngChoices(listenQ, listenPicks, revealed) { index, option -> listenPicks[index] = option } + } + if (gapRows.isNotEmpty()) { + Text(vm.tr("en_sec_gap"), style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(top = 8.dp)) + gapRows.forEachIndexed { i, row -> + Text("${i + 1}. ${row.str("prompt")}", style = MaterialTheme.typography.titleSmall) + val mark = if (!revealed) null else answerAccepts(normalizeAnswer(gapText[i]), row.str("answers")) + WordField(gapText[i], { gapText[i] = it }, "", mark) + if (revealed) row.strOrNull("meaning")?.let { Meaning(it, true) } + } + } + if (writing != null) { + Text(vm.tr("en_sec_write"), style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(top = 8.dp)) + Text(writing.str("prompt")) + OutlinedTextField( + value = essay, + onValueChange = { essay = it }, + modifier = Modifier.fillMaxWidth().height(160.dp), + minLines = 5, + ) + if (showModel) { + Text(vm.tr("en_model"), style = MaterialTheme.typography.titleSmall) + Text(writing.str("model")) + } + } + if (quiz.isNotEmpty()) { + Text(vm.tr("en_sec_quiz"), style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(top = 8.dp)) + EngChoices(quiz, quizPicks, revealed) { index, option -> quizPicks[index] = option } + } + } +} + +@Composable +private fun EngChoices( + questions: List, + picks: List, + revealed: Boolean, + onPick: (Int, Int) -> Unit, +) { + questions.forEachIndexed { i, q -> + Text("${i + 1}. ${q.str("prompt")}", style = MaterialTheme.typography.titleSmall, modifier = Modifier.padding(top = 8.dp)) + q.strs("options").forEachIndexed { o, opt -> + val selected = picks.getOrElse(i) { -1 } == o + val mark = if (revealed && selected) picks[i] == q.int("correct") else null + OptionRow(opt, selected, mark) { onPick(i, o) } + } + if (revealed) q.strOrNull("expl")?.let { Meaning(it, true) } + } } \ No newline at end of file diff --git a/android/app/src/main/java/org/maturita/maturita/ui/Icons.kt b/android/app/src/main/java/org/maturita/maturita/ui/Icons.kt index 63eea9f..2e8d5da 100644 --- a/android/app/src/main/java/org/maturita/maturita/ui/Icons.kt +++ b/android/app/src/main/java/org/maturita/maturita/ui/Icons.kt @@ -195,6 +195,26 @@ fun CzechFlag(iconSize: Dp = 48.dp) { } } +@Composable +fun UkFlag(iconSize: Dp = 48.dp) { + DrawnIcon(iconSize) { + val w = this.size.width + val h = this.size.height + val clip = Path().apply { addOval(androidx.compose.ui.geometry.Rect(0f, 0f, w, h)) } + clipPath(clip) { + drawRect(Color(0xFF012466), size = Size(w, h)) + drawLine(Color.White, Offset(0f, 0f), Offset(w, h), strokeWidth = w * 0.16f) + drawLine(Color.White, Offset(w, 0f), Offset(0f, h), strokeWidth = w * 0.16f) + drawLine(Color(0xFFCC0D26), Offset(0f, 0f), Offset(w, h), strokeWidth = w * 0.055f) + drawLine(Color(0xFFCC0D26), Offset(w, 0f), Offset(0f, h), strokeWidth = w * 0.055f) + drawRect(Color.White, topLeft = Offset(0f, h * 0.38f), size = Size(w, h * 0.24f)) + drawRect(Color.White, topLeft = Offset(w * 0.38f, 0f), size = Size(w * 0.24f, h)) + drawRect(Color(0xFFCC0D26), topLeft = Offset(0f, h * 0.43f), size = Size(w, h * 0.14f)) + drawRect(Color(0xFFCC0D26), topLeft = Offset(w * 0.43f, 0f), size = Size(w * 0.14f, h)) + } + } +} + @Composable fun BookIcon(color: Color, iconSize: Dp = 22.dp) { DrawnIcon(iconSize) { diff --git a/android/app/src/main/java/org/maturita/maturita/ui/Screens.kt b/android/app/src/main/java/org/maturita/maturita/ui/Screens.kt index ab84346..1b61a1a 100644 --- a/android/app/src/main/java/org/maturita/maturita/ui/Screens.kt +++ b/android/app/src/main/java/org/maturita/maturita/ui/Screens.kt @@ -223,6 +223,7 @@ private fun SubjectIcon(icon: String, open: Boolean) { "chip" -> ChipIcon(tint, 26.dp) "people" -> Icon(Icons.Filled.Groups, null, tint = tint, modifier = Modifier.size(26.dp)) "cz" -> CzechFlag(28.dp) + "uk" -> UkFlag(28.dp) "flask" -> Icon(Icons.Filled.Science, null, tint = tint, modifier = Modifier.size(26.dp)) "bolt" -> Icon(Icons.Filled.Bolt, null, tint = tint, modifier = Modifier.size(26.dp)) "function" -> Icon(Icons.Filled.Functions, null, tint = tint, modifier = Modifier.size(26.dp)) @@ -306,6 +307,12 @@ private fun subjectSum(vm: AppViewModel, s: J): ProgressSum { sum.totalEx = vm.content.hw.size sum.doneEx = vm.content.hw.count { vm.progress.hwDone(it.int("id")) } } + "enyears", "en1map", "en2map", "en3map", "en4map" -> { + sum.totalEx = (1..4).sumOf { vm.content.enYear(it).size } + sum.doneEx = (1..4).sumOf { year -> + vm.content.enYear(year).count { vm.progress.enDone(year, it.int("id")) } + } + } "onyears", "onmap", "on2map", "on3map", "on4map" -> { sum.totalEx = vm.content.on.size + vm.content.on2.size + vm.content.on3.size + vm.content.on4.size sum.doneEx = vm.content.on.count { vm.progress.onDone(it.int("id")) } + @@ -545,6 +552,13 @@ private fun buildSearch(vm: AppViewModel): List { add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "on4unit${l.int("id")}", extra = "obcanka") add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "on4ex${l.int("id")}") } + for (year in 1..4) { + add(vm.tr("en_year$year"), vm.tr("search_lesson"), "en${year}map", extra = "anglictina english rocnik") + vm.content.enYear(year).forEach { l -> + add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "en${year}unit${l.int("id")}", extra = "anglictina english") + add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "en${year}ex${l.int("id")}") + } + } add(vm.tr("Literatura"), vm.tr("search_lesson"), "lityears", extra = "literatura") add(vm.tr("lit_year1"), vm.tr("search_lesson"), "litmap", extra = "rocnik") add(vm.tr("lit_year2"), vm.tr("search_lesson"), "lit2map", extra = "rocnik") @@ -722,6 +736,21 @@ fun HwYearsScreen(vm: AppViewModel) { } @Composable +fun EnYearsScreen(vm: AppViewModel) { + DetailScaffold(vm, vm.tr("English"), vm.tr("en_years_sub")) { inner -> + LazyColumn(contentPadding = PaddingValues(top = inner.calculateTopPadding(), bottom = inner.calculateBottomPadding() + 16.dp)) { + items(4) { index -> + val year = index + 1 + NavRow( + title = vm.tr("en_year$year"), + subtitle = vm.tr("en_y${year}_sub"), + leading = { Text("$year.", style = MaterialTheme.typography.titleMedium) }, + ) { vm.go(Route.EnMap(year)) } + } + } + } +} + fun OnYearsScreen(vm: AppViewModel) { DetailScaffold(vm, vm.tr("Občanská nauka"), vm.tr("on_years_sub")) { inner -> LazyColumn(contentPadding = PaddingValues(top = inner.calculateTopPadding(), bottom = inner.calculateBottomPadding() + 16.dp)) { diff --git a/data/changelog.txt b/data/changelog.txt index c809291..f206109 100644 --- a/data/changelog.txt +++ b/data/changelog.txt @@ -6,6 +6,12 @@ # cs: bullet # en: bullet +--- 2026-10-07 +cs: Angličtina: čtyři ročníky, 32 lekcí podle osnov gymnázia +cs: Poslech, čtení, psaní, doplňování, kvízy a testy od A2 k maturitě +en: English: four years, 32 lessons from the gymnasium syllabus +en: Listening, reading, writing, gap-fill, quizzes and tests from A2 to the exam + --- 2026-10-06 cs: Matematika: opakování základní školy a čtyři ročníky, 40 lekcí cs: Příklady se píšou a kontrolují, u geometrie se rýsuje na mřížku diff --git a/ios/Graduately/AppModel.swift b/ios/Graduately/AppModel.swift index 34bb644..5a44f6a 100644 --- a/ios/Graduately/AppModel.swift +++ b/ios/Graduately/AppModel.swift @@ -166,6 +166,11 @@ final class AppModel: ObservableObject { refresh() } + func markEn(_ year: Int, _ id: Int) { + progress.markEn(year, id) + refresh() + } + func markLit(_ id: Int) { progress.markLit(id) refresh() diff --git a/ios/Graduately/Data/Content.swift b/ios/Graduately/Data/Content.swift index 3dc8db2..8f75410 100644 --- a/ios/Graduately/Data/Content.swift +++ b/ios/Graduately/Data/Content.swift @@ -35,6 +35,10 @@ final class Content { let on2: [J] let on3: [J] let on4: [J] + let en: [J] + let en2: [J] + let en3: [J] + let en4: [J] let lit: [J] let lit2: [J] let lit3: [J] @@ -85,6 +89,10 @@ final class Content { on2 = raw.arr("on2") on3 = raw.arr("on3") on4 = raw.arr("on4") + en = raw.arr("en") + en2 = raw.arr("en2") + en3 = raw.arr("en3") + en4 = raw.arr("en4") lit = raw.arr("lit") lit2 = raw.arr("lit2") lit3 = raw.arr("lit3") @@ -112,6 +120,15 @@ final class Content { func on2Lesson(_ id: Int) -> J? { on2.first { $0.int("id") == id } } func on3Lesson(_ id: Int) -> J? { on3.first { $0.int("id") == id } } func on4Lesson(_ id: Int) -> J? { on4.first { $0.int("id") == id } } + func enYear(_ year: Int) -> [J] { + switch year { + case 2: return en2 + case 3: return en3 + case 4: return en4 + default: return en + } + } + func enLesson(_ year: Int, _ id: Int) -> J? { enYear(year).first { $0.int("id") == id } } func litLesson(_ id: Int) -> J? { lit.first { $0.int("id") == id } } func lit2Lesson(_ id: Int) -> J? { lit2.first { $0.int("id") == id } } func lit3Lesson(_ id: Int) -> J? { lit3.first { $0.int("id") == id } } diff --git a/ios/Graduately/Data/ProgressStore.swift b/ios/Graduately/Data/ProgressStore.swift index 14a0b68..5dbf2ce 100644 --- a/ios/Graduately/Data/ProgressStore.swift +++ b/ios/Graduately/Data/ProgressStore.swift @@ -46,6 +46,9 @@ final class ProgressStore { func on4Done(_ id: Int) -> Bool { defaults.bool(forKey: "on4.\(id)") } func markOn4(_ id: Int) { defaults.set(true, forKey: "on4.\(id)") } + func enDone(_ year: Int, _ id: Int) -> Bool { defaults.bool(forKey: "en.\(year).\(id)") } + func markEn(_ year: Int, _ id: Int) { defaults.set(true, forKey: "en.\(year).\(id)") } + func litDone(_ id: Int) -> Bool { defaults.bool(forKey: "lit.\(id)") } func markLit(_ id: Int) { defaults.set(true, forKey: "lit.\(id)") } func lit2Done(_ id: Int) -> Bool { defaults.bool(forKey: "lit2.\(id)") } @@ -153,6 +156,16 @@ func summarize(_ content: Content, _ p: ProgressStore) -> ProgressSum { sum.doneUnits += 1 } } + for year in 1...4 { + for l in content.enYear(year) { + sum.totalEx += 1 + sum.openUnits += 1 + if p.enDone(year, l.int("id")) { + sum.doneEx += 1 + sum.doneUnits += 1 + } + } + } for l in content.lit { sum.totalEx += 1 sum.openUnits += 1 diff --git a/ios/Graduately/Route.swift b/ios/Graduately/Route.swift index 797474f..e1dddbf 100644 --- a/ios/Graduately/Route.swift +++ b/ios/Graduately/Route.swift @@ -36,6 +36,10 @@ enum Route: Hashable { case on4Map case on4Lesson(Int) case on4Ex(Int) + case enYears + case enMap(Int) + case enLesson(Int, Int) + case enEx(Int, Int) case litYears case litMap case litLesson(Int) @@ -123,6 +127,10 @@ enum Route: Hashable { case .on4Map: return "on4map" case .on4Lesson(let id): return "on4unit\(id)" case .on4Ex(let id): return "on4ex\(id)" + case .enYears: return "enyears" + case .enMap(let year): return "en\(year)map" + case .enLesson(let year, let id): return "en\(year)unit\(id)" + case .enEx(let year, let id): return "en\(year)ex\(id)" case .litYears: return "lityears" case .litMap: return "litmap" case .litLesson(let id): return "litunit\(id)" @@ -246,6 +254,14 @@ enum Route: Hashable { return [.onYears, .on4Map, .on4Lesson(id)] case .on4Ex(let id): return [.onYears, .on4Map, .on4Lesson(id), .on4Ex(id)] + case .enYears: + return [.enYears] + case .enMap(let year): + return [.enYears, .enMap(year)] + case .enLesson(let year, let id): + return [.enYears, .enMap(year), .enLesson(year, id)] + case .enEx(let year, let id): + return [.enYears, .enMap(year), .enLesson(year, id), .enEx(year, id)] case .litYears: return [.czechMap, .litYears] case .litMap: @@ -377,6 +393,7 @@ func routeFromPage(_ page: String) -> Route? { case "on2map": return .on2Map case "on3map": return .on3Map case "on4map": return .on4Map + case "enyears": return .enYears case "lityears": return .litYears case "litmap": return .litMap case "lit2map": return .lit2Map @@ -529,6 +546,15 @@ func routeFromPage(_ page: String) -> Route? { if let m = page.wholeMatch(of: /litex(\d+)/), let n = Int(m.1) { return .litEx(n) } + if let m = page.wholeMatch(of: /en(\d)map/), let y = Int(m.1) { + return .enMap(y) + } + if let m = page.wholeMatch(of: /en(\d)unit(\d+)/), let y = Int(m.1), let n = Int(m.2) { + return .enLesson(y, n) + } + if let m = page.wholeMatch(of: /en(\d)ex(\d+)/), let y = Int(m.1), let n = Int(m.2) { + return .enEx(y, n) + } if let m = page.wholeMatch(of: /on4unit(\d+)/), let n = Int(m.1) { return .on4Lesson(n) } diff --git a/ios/Graduately/UI/Components.swift b/ios/Graduately/UI/Components.swift index e095224..53523b9 100644 --- a/ios/Graduately/UI/Components.swift +++ b/ios/Graduately/UI/Components.swift @@ -625,6 +625,7 @@ struct SubjectIcon: View { switch icon { case "de": GermanFlag(size: size) case "cz": CzechFlag(size: size) + case "uk": UkFlag(size: size) default: Image(systemName: subjectSymbol(icon)) .font(.system(size: size * 0.72, weight: .semibold)) diff --git a/ios/Graduately/UI/ExercisesMore.swift b/ios/Graduately/UI/ExercisesMore.swift index ce75f15..1b28805 100644 --- a/ios/Graduately/UI/ExercisesMore.swift +++ b/ios/Graduately/UI/ExercisesMore.swift @@ -1,3 +1,4 @@ +import AVFoundation import SwiftUI struct DialogEx: View { @@ -1345,3 +1346,188 @@ struct PlotScreen: View { } } +private final class EnSpeaker: ObservableObject { + private let synth = AVSpeechSynthesizer() + + func speak(_ text: String) { + if synth.isSpeaking { synth.stopSpeaking(at: .immediate) } + let utterance = AVSpeechUtterance(string: text) + utterance.voice = AVSpeechSynthesisVoice(language: "en-GB") + ?? AVSpeechSynthesisVoice(language: "en-US") + utterance.rate = AVSpeechUtteranceDefaultSpeechRate * 0.9 + synth.speak(utterance) + } +} + +struct EngExScreen: View { + @ObservedObject var vm: AppModel + let year: Int + let id: Int + @StateObject private var speaker: EnSpeaker + @State private var readPicks: [Int] + @State private var listenPicks: [Int] + @State private var quizPicks: [Int] + @State private var gaps: [String] + @State private var essay = "" + @State private var played = false + @State private var showScript = false + @State private var revealed = false + @State private var showModel = false + @State private var fb = ("", "") + + init(vm: AppModel, year: Int, id: Int) { + self.vm = vm + self.year = year + self.id = id + let lesson = vm.content.enLesson(year, id) + _readPicks = State(initialValue: Array(repeating: -1, count: lesson?.arr("readQuiz").count ?? 0)) + _listenPicks = State(initialValue: Array(repeating: -1, count: lesson?.arr("listenQuiz").count ?? 0)) + _quizPicks = State(initialValue: Array(repeating: -1, count: lesson?.arr("quiz").count ?? 0)) + _gaps = State(initialValue: Array(repeating: "", count: lesson?.arr("gaps").count ?? 0)) + _speaker = StateObject(wrappedValue: EnSpeaker()) + } + + var body: some View { + if let lesson = vm.content.enLesson(year, id) { + let p = vm.palette + ExerciseScaffold(vm: vm, title: vm.tr(lesson.str("titleKey")), sub: nil, action: vm.tr("check"), onAction: { + check(lesson) + }, feedback: fb.0, kind: fb.1) { + if let reading = lesson.strOrNull("reading") { + section("en_sec_read") + Text(reading).textSelection(.enabled) + choiceBlock(lesson.arr("readQuiz"), picks: $readPicks) + } + if let listening = lesson.strOrNull("listening") { + section("en_sec_listen") + Text(vm.tr("en_listen_note")) + .font(.subheadline) + .foregroundStyle(.secondary) + PillButton(label: vm.tr("en_play")) { + played = true + showScript = false + speaker.speak(listening) + let wait = max(2.5, Double(listening.split(separator: " ").count) * 0.42) + DispatchQueue.main.asyncAfter(deadline: .now() + wait) { + showScript = true + } + } + if showScript { + Text(listening).textSelection(.enabled) + } + choiceBlock(lesson.arr("listenQuiz"), picks: $listenPicks) + } + let gapRows = lesson.arr("gaps") + if !gapRows.isEmpty { + section("en_sec_gap") + ForEach(gapRows.indices, id: \.self) { i in + Text("\(i + 1). \(gapRows[i].str("prompt"))") + .font(.body.weight(.semibold)) + WordField( + value: Binding(get: { gaps[i] }, set: { gaps[i] = $0 }), + placeholder: "", + mark: revealed ? answerAccepts(normalizeAnswer(gaps[i]), gapRows[i].str("answers")) : nil, + palette: p + ) + if revealed, let hint = gapRows[i].strOrNull("meaning") { + Meaning(text: hint, palette: p, visible: true) + } + } + } + if let writing = lesson.obj("writing") { + section("en_sec_write") + Text(writing.str("prompt")) + TextEditor(text: $essay) + .frame(minHeight: 140) + .padding(8) + .background(Color.primary.opacity(0.04), in: RoundedRectangle(cornerRadius: 12)) + if showModel { + Text(vm.tr("en_model")) + .font(.subheadline.weight(.semibold)) + Text(writing.str("model")).textSelection(.enabled) + } + } + let quiz = lesson.arr("quiz") + if !quiz.isEmpty { + section("en_sec_quiz") + choiceBlock(quiz, picks: $quizPicks) + } + } + } + } + + private func section(_ key: String) -> some View { + Text(vm.tr(key)) + .font(.title3.weight(.bold)) + .padding(.top, 8) + } + + private func choiceBlock(_ questions: [J], picks: Binding<[Int]>) -> some View { + let p = vm.palette + return ForEach(questions.indices, id: \.self) { i in + let q = questions[i] + VStack(alignment: .leading, spacing: 6) { + Text("\(i + 1). \(q.str("prompt"))") + .font(.body.weight(.semibold)) + ForEach(Array(q.strs("options").enumerated()), id: \.offset) { o, opt in + let selected = picks.wrappedValue[i] == o + let mark: Bool? = revealed && selected ? picks.wrappedValue[i] == q.int("correct") : nil + OptionChip(text: opt, selected: selected, mark: mark) { + picks.wrappedValue[i] = o + } + } + if revealed, let expl = q.strOrNull("expl") { + Meaning(text: expl, palette: p, visible: true) + } + } + .padding(.bottom, 8) + } + } + + private func check(_ lesson: J) { + if lesson.strOrNull("listening") != nil && !played { + fb = (vm.tr("en_listen_first"), "err") + return + } + let readOk = score(lesson.arr("readQuiz"), readPicks) + let listenOk = score(lesson.arr("listenQuiz"), listenPicks) + let quizOk = score(lesson.arr("quiz"), quizPicks) + let gapRows = lesson.arr("gaps") + let gapOk = gapRows.indices.filter { + answerAccepts(normalizeAnswer(gaps[$0]), gapRows[$0].str("answers")) + }.count + var writeOk = true + if let writing = lesson.obj("writing") { + let keys = writing.str("keys").split(separator: "|").map(String.init) + let enough = wordCount(essay) >= writing.int("minWords", 20) + writeOk = enough && keys.allSatisfy { hasWord(essay, $0) } + } + revealed = true + let all = readOk && listenOk && quizOk && gapOk == gapRows.count && writeOk + if all { + fb = (vm.tr("feedback_ok"), "ok") + showModel = true + vm.markEn(year, id) + } else if lesson.obj("writing") != nil && !writeOk && readOk && listenOk && quizOk && gapOk == gapRows.count { + fb = (vm.tr("en_write_short"), "err") + } else { + fb = (vm.tr("feedback_retry_short"), "err") + } + } + + private func score(_ questions: [J], _ picks: [Int]) -> Bool { + questions.indices.allSatisfy { picks[$0] == questions[$0].int("correct") } + } + + private func wordCount(_ text: String) -> Int { + let n = normalizeAnswer(text) + return n.isEmpty ? 0 : n.split(separator: " ").count + } + + private func hasWord(_ text: String, _ word: String) -> Bool { + let n = " \(normalizeAnswer(text)) " + let w = normalizeAnswer(word) + return !w.isEmpty && n.contains(" \(w) ") + } +} + diff --git a/ios/Graduately/UI/Icons.swift b/ios/Graduately/UI/Icons.swift index e45e54e..6434277 100644 --- a/ios/Graduately/UI/Icons.swift +++ b/ios/Graduately/UI/Icons.swift @@ -33,3 +33,27 @@ struct CzechFlag: View { .frame(width: size, height: size) } } + +struct UkFlag: View { + var size: CGFloat = 48 + var body: some View { + Canvas { ctx, sz in + let w = sz.width + let h = sz.height + ctx.clip(to: Path(ellipseIn: CGRect(origin: .zero, size: sz))) + ctx.fill(Path(CGRect(origin: .zero, size: sz)), with: .color(Color(red: 0.01, green: 0.14, blue: 0.40))) + var saltire = Path() + saltire.move(to: .zero) + saltire.addLine(to: CGPoint(x: w, y: h)) + saltire.move(to: CGPoint(x: w, y: 0)) + saltire.addLine(to: CGPoint(x: 0, y: h)) + ctx.stroke(saltire, with: .color(.white), lineWidth: w * 0.16) + ctx.stroke(saltire, with: .color(Color(red: 0.80, green: 0.05, blue: 0.15)), lineWidth: w * 0.055) + ctx.fill(Path(CGRect(x: 0, y: h * 0.38, width: w, height: h * 0.24)), with: .color(.white)) + ctx.fill(Path(CGRect(x: w * 0.38, y: 0, width: w * 0.24, height: h)), with: .color(.white)) + ctx.fill(Path(CGRect(x: 0, y: h * 0.43, width: w, height: h * 0.14)), with: .color(Color(red: 0.80, green: 0.05, blue: 0.15))) + ctx.fill(Path(CGRect(x: w * 0.43, y: 0, width: w * 0.14, height: h)), with: .color(Color(red: 0.80, green: 0.05, blue: 0.15))) + } + .frame(width: size, height: size) + } +} diff --git a/ios/Graduately/UI/Screens.swift b/ios/Graduately/UI/Screens.swift index 2696012..f0c109f 100644 --- a/ios/Graduately/UI/Screens.swift +++ b/ios/Graduately/UI/Screens.swift @@ -689,6 +689,11 @@ private func subjectSum(_ vm: AppModel, _ s: J) -> ProgressSum { case "hwyears", "hwmap": sum.totalEx = vm.content.hw.count sum.doneEx = vm.content.hw.filter { vm.progress.hwDone($0.int("id")) }.count + case "enyears", "en1map", "en2map", "en3map", "en4map": + sum.totalEx = (1...4).reduce(0) { $0 + vm.content.enYear($1).count } + sum.doneEx = (1...4).reduce(0) { acc, year in + acc + vm.content.enYear(year).filter { vm.progress.enDone(year, $0.int("id")) }.count + } case "onyears", "onmap", "on2map", "on3map", "on4map": sum.totalEx = vm.content.on.count + vm.content.on2.count + vm.content.on3.count + vm.content.on4.count sum.doneEx = vm.content.on.filter { vm.progress.onDone($0.int("id")) }.count @@ -905,6 +910,12 @@ private func buildSearch(_ vm: AppModel) -> [Hit] { add(vm.tr("on_year2"), vm.tr("search_lesson"), "on2map", extra: "rocnik") add(vm.tr("on_year3"), vm.tr("search_lesson"), "on3map", extra: "rocnik") add(vm.tr("on_year4"), vm.tr("search_lesson"), "on4map", extra: "rocnik") + for year in 1...4 { + add(vm.tr("en_year\(year)"), vm.tr("search_lesson"), "en\(year)map", extra: "anglictina english rocnik") + for l in vm.content.enYear(year) { + add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "en\(year)unit\(l.int("id"))", extra: "anglictina english") + } + } for l in vm.content.on { add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "onunit\(l.int("id"))", extra: "obcanka") add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "onex\(l.int("id"))") @@ -1247,6 +1258,40 @@ struct HwYearsScreen: View { } } +struct EnYearsScreen: View { + @ObservedObject var vm: AppModel + + var body: some View { + #if os(macOS) + let years: [(String, String, Route)] = [ + ("en_year1", "en_y1_sub", .enMap(1)), + ("en_year2", "en_y2_sub", .enMap(2)), + ("en_year3", "en_y3_sub", .enMap(3)), + ("en_year4", "en_y4_sub", .enMap(4)), + ] + MacFillGrid(count: years.count, minWidth: 260, minHeight: 180) { i in + NavigationLink(value: years[i].2) { + MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: "\(i + 1).") + } + .buttonStyle(.plain) + } + .navigationTitle(vm.tr("English")) + .navigationSubtitle(vm.tr("en_years_sub")) + #else + List { + ForEach(1...4, id: \.self) { year in + NavigationLink(value: Route.enMap(year)) { + ListRowLabel(title: vm.tr("en_year\(year)"), subtitle: vm.tr("en_y\(year)_sub")) + } + } + } + .navigationTitle(vm.tr("English")) + .navigationSubtitle(vm.tr("en_years_sub")) + .appListStyle() + #endif + } +} + struct OnYearsScreen: View { @ObservedObject var vm: AppModel @@ -2183,6 +2228,29 @@ struct RouteDestination: View { HwQuizScreen(vm: vm, id: id) case .onYears: OnYearsScreen(vm: vm) + case .enYears: + EnYearsScreen(vm: vm) + case .enMap(let year): + LessonListScreen( + vm: vm, + title: vm.tr("en_year\(year)"), + subtitle: vm.tr("en_y\(year)_sub"), + rows: vm.content.enYear(year).map { l in + let id = l.int("id") + return LessonRow( + id: "e\(year)\(id)", + title: vm.tr(l.str("titleKey")), + done: vm.progress.enDone(year, id), + destination: .enLesson(year, id) + ) + } + ) + case .enLesson(let year, let id): + if let l = vm.content.enLesson(year, id) { + SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .enEx(year, id)) + } + case .enEx(let year, let id): + EngExScreen(vm: vm, year: year, id: id) case .onMap: LessonListScreen( vm: vm, diff --git a/scripts/extract-android-content.py b/scripts/extract-android-content.py index 422e339..b597932 100644 --- a/scripts/extract-android-content.py +++ b/scripts/extract-android-content.py @@ -776,7 +776,10 @@ def main() -> int: arrays: dict[str, tuple[str, list]] = {} i18n = {} for path in sorted(SRC.glob("*.c")): - text = strip_comments(path.read_text(encoding="utf-8")) + raw = path.read_text(encoding="utf-8") + if path.name == "english.c": + raw = expand_local_includes(raw, path.parent) + text = strip_comments(raw) arrays.update(find_arrays(text)) if path.name == "i18n.c": i18n = parse_i18n(strip_comments(expand_local_includes(text, path.parent))) @@ -1606,13 +1609,66 @@ def main() -> int: ex_lines.append("") (SRC / "cetba_book_exercises.inc").write_text("\n".join(ex_lines) + "\n", encoding="utf-8") + def en_text(name): + if name not in arrays: + return None + raw = A(name) + if isinstance(raw, list): + parts = [x for x in raw if isinstance(x, str)] + return parts[0] if len(parts) == 1 else ("\n".join(parts) if parts else None) + return raw if isinstance(raw, str) else None + + def en_choice(name): + if name not in arrays: + return [] + quiz = choice_items(A(name)) + hint_name = name + "_hints" + hints = str_list(A(hint_name)) if hint_name in arrays else [] + for qi, q in enumerate(quiz): + if qi < len(hints) and hints[qi]: + q["expl"] = hints[qi] + return quiz + + def en_year(year): + lessons = [] + for i in range(1, 9): + prefix = f"en_y{year}_l{i}" + write = str_list(A(f"{prefix}_write")) if f"{prefix}_write" in arrays else [] + writing = None + if len(write) >= 3: + writing = { + "prompt": write[0], + "model": write[1], + "keys": write[2], + "minWords": int(write[3]) if len(write) > 3 and str(write[3]).isdigit() else 20, + } + lessons.append({ + "id": i, + "titleKey": f"en_y{year}_u{i}", + "subKey": f"en_y{year}_u{i}_sub", + "slides": slides_items(A(f"{prefix}_slides")), + "reading": en_text(f"{prefix}_read"), + "readQuiz": en_choice(f"{prefix}_readq"), + "listening": en_text(f"{prefix}_listen"), + "listenQuiz": en_choice(f"{prefix}_listenq"), + "gaps": typed_items(A(f"{prefix}_gaps")) if f"{prefix}_gaps" in arrays else [], + "writing": writing, + "quiz": en_choice(f"{prefix}_quiz"), + }) + return lessons + + en_lessons = en_year(1) + en2_lessons = en_year(2) + en3_lessons = en_year(3) + en4_lessons = en_year(4) + subjects = [ {"key": "Deutsch", "open": True, "target": "roadmap", "icon": "de"}, {"key": "Správa počítačových sítí", "open": True, "target": "netyears", "icon": "wifi"}, {"key": "Technické vybavení", "open": True, "target": "hwyears", "icon": "chip"}, {"key": "Český jazyk a literatura", "open": True, "target": "czechmap", "icon": "cz"}, {"key": "Občanská nauka", "open": True, "target": "onyears", "icon": "people"}, - {"key": "English", "open": False, "target": None, "icon": "lock"}, + {"key": "English", "open": True, "target": "enyears", "icon": "uk"}, {"key": "Matematika", "open": True, "target": "matyears", "icon": "function"}, {"key": "Fyzika", "open": True, "target": "fyzyears", "icon": "bolt"}, {"key": "Základy Přírodopisných věd", "open": True, "target": "scimap", "icon": "flask"}, @@ -1651,6 +1707,10 @@ def main() -> int: "on2": on2_lessons, "on3": on3_lessons, "on4": on4_lessons, + "en": en_lessons, + "en2": en2_lessons, + "en3": en3_lessons, + "en4": en4_lessons, "mluvnice": mluvnice, "books": books, "changelog": parse_changelog(ROOT / "data" / "changelog.txt"), diff --git a/scripts/gen-english.py b/scripts/gen-english.py new file mode 100644 index 0000000..aeabc20 --- /dev/null +++ b/scripts/gen-english.py @@ -0,0 +1,2537 @@ +#!/usr/bin/env python3 +"""Generate src/english_lessons.inc and src/english_i18n.inc. + +Curriculum: four-year gymnasium English (RVP G, cizí jazyk, aiming at +maturita B1 and the CERMAT catalogue). Year 1 is A2, year 2 B1, +year 3 B1+, year 4 B2 exam skills. Texts are original. +""" + +from __future__ import annotations + +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +def norm(s: str) -> str: + out = [] + space = False + for ch in s.lower(): + if ch.isalnum(): + out.append(ch) + space = False + elif ch.isspace(): + if out and not space: + out.append(" ") + space = True + return "".join(out).strip() + + +def has_word(text: str, word: str) -> bool: + n = f" {norm(text)} " + w = norm(word) + return f" {w} " in n + + +def words(text: str) -> int: + n = norm(text) + return len(n.split()) if n else 0 + + +def c_str(s: str) -> str: + esc = ( + s.replace("\\", "\\\\") + .replace('"', '\\"') + .replace("\n", "\\n") + .replace("\t", "\\t") + ) + return f'"{esc}"' + + +def Q(prompt, options, correct, hint): + if len(options) != 4: + raise SystemExit(f"need 4 options: {prompt}") + if not 0 <= correct < 4: + raise SystemExit(f"bad correct: {prompt}") + return (prompt, options, correct, hint) + + +def G(prompt, answers, meaning): + return (prompt, answers, meaning) + + +def W(prompt, model, keys, min_words): + for key in keys.split("|"): + if not has_word(model, key): + raise SystemExit(f"model misses keyword {key!r}: {prompt[:40]}") + if words(model) < min_words: + raise SystemExit(f"model shorter than {min_words}: {prompt[:40]}") + return (prompt, model, keys, min_words) + + +def S(kicker, title, tip, lines): + return (kicker, title, tip, lines) + + +def L(title_cs, title_en, sub_cs, sub_en, slides, **parts): + return { + "title_cs": title_cs, + "title_en": title_en, + "sub_cs": sub_cs, + "sub_en": sub_en, + "slides": slides, + **parts, + } + + +YEARS = [ + { + "sub_cs": "Osobní život, škola, domov a minulost. Úroveň A2.", + "sub_en": "Personal life, school, home and the past. Level A2.", + "lessons": [ + L( + "Já a moje rodina", + "Me and my family", + "Sloveso be, have got a přivlastňovací zájmena. Doplňování a kvíz.", + "The verb be, have got and possessives. Gaps and a quiz.", + [ + S( + "1 / 2 • Be", + "Kdo jsem", + "Tip: věk se říká slovesem be. „She has 16 years“ je čechismus.", + [ + "I am, you are, he/she/it is, we/you/they are.", + "Zápor a otázka: She isn't tall. Are you from Prague?", + "Věk, povolání i nálada: I am 16. My mum is a nurse. I am fine.", + "Přivlastňovací zájmena: my, your, his, her, its, our, their.", + ], + ), + S( + "2 / 2 • Have got", + "Co mám", + "Tip: have got je britská hovorová vazba pro vlastnictví, ne pro věk.", + [ + "I've got a younger brother. She hasn't got a pet.", + "Have you got any cousins? — Yes, I have. / No, I haven't.", + "V americké angličtině stačí have: I have a brother.", + "Rodina: parents, siblings, only child, get on well, look like.", + ], + ), + ], + gaps=[ + G("My sister ___ sixteen.", "is", "Věk: be, ne have."), + G("___ you got a bike?", "Have", "Otázka s have got."), + G("This is ___ grandmother. She lives with us.", "our", "Přivlastňovací zájmeno."), + G("They ___ not from Brno. They are from Olomouc.", "are", "They + are."), + G("He ___ got a dog, but he has got two cats.", "hasn't|has not", "Zápor have got."), + ], + quiz=[ + Q( + "Which sentence is correct?", + ["She has 16 years.", "She is 16 years old.", "She is 16 years.", "She has 16 years old."], + 1, + "Věk: be + years old.", + ), + Q( + "I ___ two cousins and one aunt.", + ["am", "have got", "has got", "is"], + 1, + "Vlastnictví v 1. osobě: have got.", + ), + Q( + "Eva is my sister. ___ room is next to mine.", + ["She", "Her", "His", "Hers"], + 1, + "Před podstatným jménem je her, ne she.", + ), + Q( + "___ your parents at home?", + ["Is", "Are", "Have", "Do"], + 1, + "Parents je množné číslo: Are.", + ), + ], + ), + L( + "Školní den", + "A school day", + "Přítomný čas prostý. Čtení a doplňování.", + "The present simple. Reading and gaps.", + [ + S( + "1 / 2 • Present simple", + "Co dělám pravidelně", + "Tip: ve 3. osobě jednotného čísla přibývá -s/-es: she watches.", + [ + "I get up at seven. He gets up at six.", + "Zápor: I don't like maths. She doesn't eat meat.", + "Otázka: Do you walk to school? Does she play the piano?", + "Příslovce četnosti stojí před slovesem: I usually walk. She is always late.", + ], + ), + S( + "2 / 2 • Škola", + "Rozvrh a přestávky", + "Tip: předmět ve škole je bez členu, když mluvíme obecně: I like biology.", + [ + "lessons, break, canteen, homework, timetable, subject.", + "Čas: at eight, at half past twelve, in the morning, on Monday.", + "because vysvětluje důvod: I like biology because we do experiments.", + "never / sometimes / often / usually / always.", + ], + ), + ], + reading=( + "My school day starts early. I get up at half past six, have breakfast " + "and catch the tram. Lessons begin at eight. On Monday we have maths, " + "English and biology. I like biology because we do experiments, but I " + "don't like maths. We have lunch at half past twelve in the canteen. " + "After school I do my homework or meet friends. I never go to bed after " + "ten on a school night." + ), + readq=[ + Q( + "What time do lessons begin?", + ["At half past six", "At eight", "At half past twelve", "At ten"], + 1, + "Lessons begin at eight.", + ), + Q( + "Why does the writer like biology?", + [ + "Because it is easy", + "Because they do experiments", + "Because the teacher is funny", + "Because there is no homework", + ], + 1, + "because we do experiments.", + ), + Q( + "Where do they have lunch?", + ["At home", "On the tram", "In the canteen", "In the park"], + 2, + "in the canteen.", + ), + Q( + "On a school night the writer goes to bed…", + ["after eleven", "at midnight", "never after ten", "before six"], + 2, + "never go to bed after ten.", + ), + ], + gaps=[ + G("She ___ (watch) a series every evening.", "watches", "3. osoba: watches."), + G("We ___ (not go) to school on Sunday.", "don't go|do not go", "Zápor v 1. osobě množného čísla."), + G("___ he play football on Fridays?", "Does", "Otázka ve 3. osobě."), + G("I usually ___ (get) up at seven.", "get", "Usually nemění tvar slovesa."), + ], + ), + L( + "Víkend ve městě", + "A weekend in town", + "Přítomný čas průběhový pro domluvené plány. Poslech.", + "The present continuous for arrangements. Listening.", + [ + S( + "1 / 2 • Continuous", + "Co právě probíhá a co je domluvené", + "Tip: I'm meeting Eva at ten je plán, ne jen „potkávám“.", + [ + "Tvar: am/is/are + -ing. I'm waiting. She isn't coming.", + "Teď: Look, it is raining. Please be quiet, the baby is sleeping.", + "Blízký plán: We're having lunch on Saturday. I'm playing volleyball.", + "Stavová slovesa obvykle průběhový čas nemají: I know, I like, I want.", + ], + ), + S( + "2 / 2 • Poslech", + "Jak poslouchat vzkaz", + "Tip: nejdřív si přečtěte otázky, pak teprve pusťte nahrávku.", + [ + "Chyťte jména, časy a místa. Ta se v otázkách opakují.", + "Nenechte se zastavit jedním slovem. Poslouchejte dál.", + "I'm meeting, we're going, I'm playing jsou v tomhle vzkazu klíčové.", + "Na konci bývá prosba: Call me back.", + ], + ), + ], + listening=( + "Hi Tom, it's Anna. Are you free on Saturday? I'm meeting Eva at the " + "station at ten. We're going to the market first, and then we're having " + "lunch in that new café by the river. In the afternoon I'm playing " + "volleyball with my class in the park. Do you want to come to the café? " + "Call me back tonight." + ), + listenq=[ + Q("Who is Anna meeting?", ["Tom", "Eva", "Her class", "Her mum"], 1, "I'm meeting Eva."), + Q( + "Where are they meeting?", + ["At school", "At the station", "In the park", "By the river only"], + 1, + "at the station at ten.", + ), + Q( + "What is Anna doing in the afternoon?", + ["Going to the market", "Having lunch", "Playing volleyball", "Sleeping"], + 2, + "I'm playing volleyball.", + ), + Q( + "What should Tom do?", + ["Buy tickets", "Call her back", "Come to school", "Bring a map"], + 1, + "Call me back tonight.", + ), + ], + gaps=[ + G("Listen, the phone ___.", "is ringing", "Děje se to teď."), + G("We ___ lunch with Eva on Saturday.", "are having|'re having", "Domluvený plán."), + G("I ___ not coming to the match.", "am|'m", "Zápor: am not."), + G("She ___ volleyball this afternoon.", "is playing|'s playing", "Domluvená aktivita."), + ], + ), + L( + "Můj pokoj", + "My room", + "There is / there are a předložky místa. Krátký popis a kvíz.", + "There is / there are and prepositions. A short description and a quiz.", + [ + S( + "1 / 2 • There is", + "Co v pokoji je", + "Tip: there is pro jedno, there are pro více. There je formální podmět.", + [ + "There is a desk under the window. There are two chairs.", + "Zápor: There isn't a TV. There aren't any posters.", + "Otázka: Is there a lamp? Are there any books?", + "Předložky: in, on, under, next to, between, behind, in front of.", + ], + ), + S( + "2 / 2 • Psaní", + "Popis místa", + "Tip: jděte od velkého k malému, od dveří k oknu.", + [ + "Začněte velikostí a světlem: small but bright.", + "Každá věta ať má věc a místo: The bag is under the chair.", + "Střídejte there is a there are, ať text není seznam.", + "Na konci jedna osobní věta: I like it because it is quiet.", + ], + ), + ], + writing=W( + "Describe your room in at least 30 words. Say what is in it and where things are. Use on and a piece of furniture such as a bed, and mention a window.", + "My room is small but bright. There is a bed next to the window and a wooden desk under a shelf. My books are on the desk and my school bag is under the chair. There are two posters on the wall. I like the room because it is quiet in the evening.", + "bed|window|on", + 30, + ), + quiz=[ + Q( + "The lamp is ___ the desk.", + ["under", "on", "between", "at"], + 1, + "Lampa stojí na desce: on.", + ), + Q( + "There ___ a big window.", + ["are", "is", "be", "have"], + 1, + "Jedno okno: there is.", + ), + Q( + "There ___ two posters on the wall.", + ["is", "are", "has", "been"], + 1, + "Dva plakáty: there are.", + ), + Q( + "The cat is sleeping ___ the bed.", + ["under", "next", "between to", "at to"], + 0, + "Pod postelí: under the bed.", + ), + ], + ), + L( + "Na trhu", + "At the market", + "Počitatelnost, some a any, how much a how many. Čtení a doplňování.", + "Countability, some and any, how much and how many. Reading and gaps.", + [ + S( + "1 / 2 • Množství", + "Počitatelné a nepočitatelné", + "Tip: some v kladné větě, any v otázce a záporu. Nabídka je výjimka: Would you like some tea?", + [ + "Počitatelné: an apple, some apples. Nepočitatelné: some bread, some milk.", + "How many apples? How much milk?", + "a little milk, a few rolls, a lot of cheese.", + "V obchodě: How much is it? It's four euros a kilo.", + ], + ), + S( + "2 / 2 • Dialog", + "Zdvořilá žádost", + "Tip: Can I have… a I'd like… zní v obchodě přirozeně.", + [ + "Have you got any bread? — Sorry, we haven't got any today.", + "We've got some rolls instead.", + "How many would you like? — Six, please.", + "Anything else? — No, that's all. Thank you.", + ], + ), + ], + reading=( + "On Saturday morning the market by the river is busy. A woman asks, " + "\"How much is this cheese?\" The seller says, \"It's four euros a kilo.\" " + "Then she asks, \"Have you got any bread?\" He answers, \"Sorry, we " + "haven't got any bread today, but we've got some fresh rolls.\" " + "\"How many rolls do you want?\" \"Six, please, and a little milk.\" " + "She also buys a few apples. \"Anything else?\" \"No, that's all.\"" + ), + readq=[ + Q( + "How much is the cheese?", + ["Four euros a piece", "Four euros a kilo", "Six euros", "A little"], + 1, + "four euros a kilo.", + ), + Q( + "What hasn't the seller got?", + ["Rolls", "Milk", "Bread", "Apples"], + 2, + "haven't got any bread today.", + ), + Q( + "How many rolls does the woman want?", + ["Four", "A few", "Six", "A kilo"], + 2, + "Six, please.", + ), + Q( + "Which phrase is an offer of more shopping?", + ["That's all.", "Anything else?", "How much is it?", "A little milk."], + 1, + "Anything else? nabízí další nákup.", + ), + ], + gaps=[ + G("We haven't got ___ bread today.", "any", "Zápor: any."), + G("There are ___ rolls in the bag.", "some", "Kladná věta: some."), + G("How ___ apples do you want?", "many", "Apples se dají počítat."), + G("How ___ milk is in the bottle?", "much", "Milk je nepočitatelné."), + ], + ), + L( + "Minulé léto", + "Last summer", + "Minulý čas prostý, pravidelná i nepravidelná slovesa. Poslech a kvíz.", + "The past simple, regular and irregular verbs. Listening and a quiz.", + [ + S( + "1 / 2 • Past simple", + "Co se stalo a je dokončené", + "Tip: zápor a otázka používají did, sloveso zůstává v základním tvaru.", + [ + "Pravidelná: play → played, visit → visited, try → tried, stop → stopped.", + "Nepravidelná: go → went, take → took, swim → swam, buy → bought.", + "I didn't stay in a hotel. Did you visit the island?", + "Signály: yesterday, last July, ago, in 2019, when I was ten.", + ], + ), + S( + "2 / 2 • Vyprávění", + "Jedna událost za druhou", + "Tip: then, after that a one day drží příběh pohromadě.", + [ + "Začněte kdy a kam: Last July we went to Croatia.", + "Pak doprava, počasí a jedna konkrétní příhoda.", + "Nepravidelná slovesa se neučí po jednom u testu, ale ve větě.", + "Na konci dojem: It was the best week of the year.", + ], + ), + ], + listening=( + "Last July we went to Croatia. We travelled by train and then by bus " + "because the flight was too expensive. The weather was hot every day. " + "I swam in the morning and my brother tried surfing. He fell off the " + "board a lot, but he loved it. One day we visited a small island. We " + "didn't stay in a hotel. We stayed with our uncle near the beach. I " + "took hundreds of photos." + ), + listenq=[ + Q("Where did they go?", ["Italy", "Croatia", "Greece", "Spain"], 1, "went to Croatia."), + Q( + "How did they travel?", + ["By plane", "By train and bus", "By car", "By ferry only"], + 1, + "by train and then by bus.", + ), + Q( + "What did the brother try?", + ["Swimming", "Surfing", "Cooking", "Driving"], + 1, + "tried surfing.", + ), + Q( + "Where did they stay?", + ["In a hotel", "On the island", "With their uncle", "On the train"], + 2, + "stayed with our uncle.", + ), + ], + quiz=[ + Q("go → ___", ["goed", "went", "gone", "going"], 1, "go, went, gone."), + Q("I ___ a lot of photos.", ["taked", "took", "taken", "token"], 1, "take, took, taken."), + Q("She ___ in the sea every morning.", ["swimmed", "swam", "swum", "swims"], 1, "Minulý tvar swim je swam."), + Q("We ___ stay in a hotel.", ["don't", "didn't", "wasn't", "haven't"], 1, "Zápor minulého času: didn't + sloveso."), + ], + ), + L( + "Zpráva kamarádovi", + "A message to a friend", + "Will a going to. Krátká neformální zpráva a doplňování.", + "Will and going to. A short informal message and gaps.", + [ + S( + "1 / 2 • Budoucnost", + "Plán, nebo rozhodnutí teď", + "Tip: going to má důkaz nebo plán. Will je rozhodnutí v tu chvíli nebo odhad.", + [ + "We've got tickets. We're going to leave at six. To je plán.", + "Look at those clouds. It's going to rain. To je důkaz.", + "The phone is ringing. I'll answer it. Rozhodnutí teď.", + "I think I'll stay at home. Will jako názor, ne jistý plán.", + ], + ), + S( + "2 / 2 • Zpráva", + "Neformální styl", + "Tip: Hi, kontraktované tvary a na konci otázka kamarádovi.", + [ + "Začněte Hi a jménem, ne Dear Sir.", + "Napište kdy, co a proč. Jedna věta s because stačí.", + "I'm going to… je v takové zprávě přirozené.", + "Konec: Want to come? Write back.", + ], + ), + ], + writing=W( + "Write a short message to a friend about next weekend. Use at least 25 words. Say what you are going to do, mention the weekend, and give a reason with because.", + "Hi Lena, next weekend I am going to visit my cousins in Pilsen. We are going to the cinema on Saturday because a new film is on. On Sunday we are going to cook lunch together. Do you want to come with me? Write back soon.", + "going|weekend|because", + 25, + ), + gaps=[ + G("Look at those clouds. It ___ rain.", "is going to|'s going to", "Je vidět důkaz: going to."), + G("I think I ___ stay at home tonight.", "will|'ll", "Názor v tu chvíli: will."), + G("The tickets are in my bag. We ___ leave at six.", "are going to|'re going to", "Už je to domluvené."), + G("Don't worry. I ___ help you with the bags.", "will|'ll", "Rozhodnutí teď: will."), + ], + ), + L( + "Test 1. ročníku", + "Year 1 test", + "Čtení, poslech, gramatika a krátké psaní. Úroveň A2.", + "Reading, listening, grammar and a short piece of writing. Level A2.", + [ + S( + "1 / 2 • Co test bere", + "A2 v jedné sadě", + "Tip: nejdřív otázky, pak text nebo nahrávku. U psaní počítejte slova.", + [ + "Čtení: najděte v textu čas, místo a důvod, nehádejte z jednoho slova.", + "Poslech: jména a čísla si poznamenejte hned.", + "Gramatika: be, přítomné časy, some/any, minulý čas, will a going to.", + "Psaní: krátký souvislý text, ne jen odrážky.", + ], + ), + S( + "2 / 2 • Jak opravovat", + "Nejčastější čechismy", + "Tip: po kontrole si přečtěte nápovědy, i když je věta správně.", + [ + "I am 16, ne I have 16 years.", + "She likes, ne she like.", + "Did you went je špatně. Did you go.", + "There is a book, ne It is a book on the table, když věc uvádíte.", + ], + ), + ], + reading=( + "Klara lives in a flat with her parents and a small dog. On weekdays " + "she walks to school because it is only ten minutes away. She loves " + "art. Last Friday her class visited a gallery and she bought a poster. " + "This Saturday she is meeting her cousin at the station. They are going " + "to a concert in the evening." + ), + readq=[ + Q( + "How does Klara get to school?", + ["By tram", "She walks", "Her dad drives", "By train"], + 1, + "she walks to school.", + ), + Q( + "What did she buy at the gallery?", + ["A dog", "A ticket", "A poster", "A flat"], + 2, + "she bought a poster.", + ), + Q( + "Who is she meeting on Saturday?", + ["Her parents", "Her art teacher", "Her cousin", "The singer"], + 2, + "meeting her cousin.", + ), + ], + listening=( + "Hi, it's Marek. I missed you after school. I'm at the sports centre. " + "The match starts at four, not at three, so don't hurry. Can you bring " + "my blue jacket? It's on the chair in the kitchen. See you there." + ), + listenq=[ + Q("Where is Marek?", ["At school", "At the sports centre", "In the kitchen", "On the tram"], 1, "at the sports centre."), + Q("What time does the match start?", ["At three", "At four", "At five", "At ten"], 1, "starts at four, not at three."), + Q("What should his friend bring?", ["A ticket", "A blue jacket", "Lunch", "A poster"], 1, "bring my blue jacket."), + ], + quiz=[ + Q("She ___ coffee.", ["don't like", "doesn't like", "not like", "isn't like"], 1, "3. osoba: doesn't like."), + Q("There ___ any milk.", ["isn't", "aren't", "hasn't", "don't"], 0, "Milk je jednotné: there isn't."), + Q("Yesterday we ___ to the lake.", ["go", "went", "gone", "going"], 1, "Minulý tvar go je went."), + Q("I've got the tickets. We ___ leave at noon.", ["will", "are going to", "did", "are"], 1, "Plán s důkazem: going to."), + ], + writing=W( + "Write at least 30 words about your family. Use is or are, and the words family and because.", + "My family is not very big. I live with my parents and my younger sister. She is twelve and she is funny. We get on well because we both like films. My dad is a driver and my mum is a nurse. I am happy at home.", + "family|because", + 30, + ), + ), + ], + }, + { + "sub_cs": "Cestování, zdraví, práce, média a příroda. Úroveň B1.", + "sub_en": "Travel, health, work, the media and nature. Level B1.", + "lessons": [ + L( + "Na cestách", + "On the road", + "Stupňování a minulý čas průběhový. Doplňování a kvíz.", + "Comparison and the past continuous. Gaps and a quiz.", + [ + S( + "1 / 2 • Stupňování", + "Porovnat dvě věci i celou skupinu", + "Tip: kratší přídavná jména berou -er/-est, delší more/most.", + [ + "cheap → cheaper → the cheapest. Expensive → more expensive → the most expensive.", + "Nepravidelné: good → better → the best, bad → worse → the worst, far → further → the furthest.", + "as … as: The bus is as fast as the train today.", + "than stojí po komparativu: This hostel is cheaper than the hotel.", + ], + ), + S( + "2 / 2 • Past continuous", + "Děje, který probíhal, když se stalo něco jiného", + "Tip: průběhový čas je pozadí, prostý je krátká událost.", + [ + "I was reading when the train stopped.", + "While we were waiting, it started to rain.", + "Zápor: We weren't sleeping. Otázka: Were you driving?", + "Nepleťte si ho s minulým prostým: I walked home popisuje celou cestu.", + ], + ), + ], + gaps=[ + G("This ticket is ___ (cheap) than the night train.", "cheaper", "Krátké přídavné jméno: -er."), + G("It was the ___ (bad) hotel in the town.", "worst", "bad, worse, the worst."), + G("Prague is not as ___ as London.", "big", "as + základní tvar + as."), + G("I ___ (read) when the bus suddenly stopped.", "was reading", "Pozadí: past continuous."), + G("While we ___ (wait), a street musician started to play.", "were waiting", "While + průběhový děj."), + ], + quiz=[ + Q("far → ___ → the furthest", ["farer", "further", "more far", "farthest only"], 1, "far, further, the furthest. Farthest je taky možné, ale further je v učebnici první."), + Q( + "Which sentence is correct?", + [ + "This café is more cheap than that one.", + "This café is cheaper than that one.", + "This café is the cheaper than that one.", + "This café is cheap than that one.", + ], + 1, + "cheap → cheaper than.", + ), + Q( + "She ___ a map when she missed the stop.", + ["looked", "was looking", "looks", "is looking"], + 1, + "Dělo se to v okamžiku, kdy zmeškala zastávku.", + ), + Q( + "good → better → ___", + ["the goodest", "the better", "the best", "the most good"], + 2, + "the best.", + ), + ], + ), + L( + "U lékaře", + "At the clinic", + "Should, must a have to. Čtení letáku a kvíz.", + "Should, must and have to. A leaflet and a quiz.", + [ + S( + "1 / 2 • Modální slovesa", + "Rada, povinnost, zákaz", + "Tip: must je silný pocit mluvčího, have to je pravidlo zvenku.", + [ + "You should rest. You shouldn't go to school tomorrow. To je rada.", + "I must call the doctor. Mluvčí cítí, že to je nutné.", + "You have to show your insurance card. To chce ordinace.", + "Mustn't je zákaz. Don't have to znamená, že to není nutné.", + ], + ), + S( + "2 / 2 • Tělo a příznaky", + "Jak to říct u přepážky", + "Tip: I've got a sore throat zní přirozeněji než I have pain in throat.", + [ + "a headache, a sore throat, a cough, a temperature, a rash.", + "It hurts when I swallow. I feel dizzy.", + "How long have you had it? — Since Monday. / For three days.", + "Take these tablets twice a day. Drink plenty of water.", + ], + ), + ], + reading=( + "The school clinic is open from seven to three. If you feel ill, you " + "should tell your teacher first. You have to bring your insurance card. " + "You must not come to school with a high temperature. Students with a " + "cough should wear a mask in the waiting room. You don't have to make " + "an appointment for a small cut, but you should come before noon. The " + "nurse can give you a painkiller. She can't give you antibiotics." + ), + readq=[ + Q( + "What do you have to bring?", + ["A mask", "An insurance card", "Antibiotics", "An appointment"], + 1, + "have to bring your insurance card.", + ), + Q( + "What is forbidden?", + [ + "Coming with a high temperature", + "Telling the teacher", + "Wearing a mask", + "Coming before noon", + ], + 0, + "must not come with a high temperature.", + ), + Q( + "You don't have to make an appointment if…", + [ + "you need antibiotics", + "you have a small cut", + "you have a temperature", + "you arrive at four", + ], + 1, + "for a small cut.", + ), + Q( + "What can't the nurse give you?", + ["Water", "A painkiller", "Antibiotics", "A mask"], + 2, + "She can't give you antibiotics.", + ), + ], + quiz=[ + Q( + "You ___ shout in the waiting room. It's a rule.", + ["don't have to", "mustn't", "shouldn't to", "haven't"], + 1, + "Zákaz: mustn't.", + ), + Q( + "You ___ wear a tie. Nobody minds.", + ["mustn't", "don't have to", "must", "should to"], + 1, + "Není to nutné: don't have to.", + ), + Q( + "I've got a headache. You ___ drink some water and rest.", + ["should", "must to", "have", "are"], + 0, + "Rada: should.", + ), + Q( + "Visitors ___ show a card. The clinic asks for it.", + ["have to", "should to", "don't must", "are have"], + 0, + "Pravidlo zvenku: have to.", + ), + ], + ), + L( + "První brigáda", + "A first job", + "Předpřítomný čas, for a since. Poslech pohovoru a doplňování.", + "The present perfect, for and since. A job interview and gaps.", + [ + S( + "1 / 2 • Present perfect", + "Zkušenost a děj až do teď", + "Tip: have/has + příčestí. Otázka na zkušenost začíná Have you ever…?", + [ + "I have worked in a café. She has never had a paid job.", + "Ever v otázce, never v záporném významu: Have you ever used a till?", + "For three months = jak dlouho. Since May = od kdy.", + "Just, already, yet: I've just finished. I haven't finished yet.", + ], + ), + S( + "2 / 2 • Pohovor", + "Krátké odpovědi", + "Tip: odpovězte a přidejte jednu konkrétní větu, ať to není jen yes.", + [ + "I'm reliable and I get on well with people.", + "I have looked after my neighbours' children.", + "I can work on Saturdays, but I can't work on Sunday mornings.", + "Could you tell me what the shift is?", + ], + ), + ], + listening=( + "Manager: Have you ever worked in a shop? Tereza: Not in a shop, but " + "I have helped in my uncle's bakery since May. Manager: What do you do " + "there? Tereza: I have served customers and I have learned to use the " + "till. I haven't baked bread yet. Manager: Can you start next Saturday? " + "Tereza: Yes. I have already asked my uncle, and he says it's fine. " + "I can't work on Sunday mornings because I have a match." + ), + listenq=[ + Q( + "Where has Tereza helped?", + ["In a supermarket", "In her uncle's bakery", "In a hotel", "At school"], + 1, + "in my uncle's bakery.", + ), + Q( + "Since when?", + ["Since March", "Since May", "Since Saturday", "For a year"], + 1, + "since May.", + ), + Q( + "What hasn't she done yet?", + ["Served customers", "Used the till", "Baked bread", "Asked her uncle"], + 2, + "I haven't baked bread yet.", + ), + Q( + "Why can't she work on Sunday mornings?", + ["The bakery is closed", "She has a match", "She is ill", "She has no till"], + 1, + "because I have a match.", + ), + ], + gaps=[ + G("She ___ (live) here since 2019.", "has lived|'s lived", "Since + předpřítomný čas."), + G("I have known him ___ three years.", "for", "For + délka."), + G("Have you ___ (be) to London?", "been", "Been to = mít zkušenost."), + G("We haven't finished ___.", "yet", "Yet v záporu na konci."), + G("He has ___ eaten, so he isn't hungry.", "already|just", "Already nebo just."), + ], + ), + L( + "Recenze filmu", + "A film review", + "Vztažné věty who, which a that. Krátká recenze a kvíz.", + "Relative clauses with who, which and that. A short review and a quiz.", + [ + S( + "1 / 2 • Vztažné věty", + "Která osoba, která věc", + "Tip: who pro lidi, which pro věci. That jde často pro obojí v definující větě.", + [ + "The actor who plays the captain is brilliant.", + "A lighthouse which stands on a black rock.", + "V definující větě se čárka nepíše: The boy who lives next door.", + "Nedefinující věta čárku má a that se v ní nepoužívá: Prague, which I love, …", + ], + ), + S( + "2 / 2 • Recenze", + "Názor, ne jen děj", + "Tip: neprozrazujte konec. Napište, pro koho film je.", + [ + "Jedna věta o ději, jedna o postavě, jedna o tom, co se povedlo.", + "I would recommend it to anyone who likes quiet films.", + "The ending, which I will not describe, stays with you.", + "Hodnocení ať je konkrétní: the music is too loud, the script is sharp.", + ], + ), + ], + writing=W( + "Write a short review of a film or a book in at least 40 words. Use who once and which once, and say who you would recommend it to.", + "The Lighthouse Keeper is a quiet film which follows a woman on a small island. The actor who plays her hardly speaks, but her face tells the story. The sea, which is almost a character, is beautiful and dangerous. I would recommend it to anyone who likes slow films. The ending is sad but honest.", + "who|which|recommend", + 40, + ), + quiz=[ + Q( + "The nurse ___ helped me was very calm.", + ["which", "who", "where", "whose it"], + 1, + "Člověk: who.", + ), + Q( + "The app ___ crashed is useless.", + ["who", "which", "where", "whom"], + 1, + "Věc: which.", + ), + Q( + "Prague, ___ is my home city, is full of tourists in July.", + ["that", "which", "who", "what"], + 1, + "S čárkou that nepatří. Which.", + ), + Q( + "That's the boy ___ dog ran into the road.", + ["who", "which", "whose", "that his"], + 2, + "Čí: whose.", + ), + ], + ), + L( + "Počasí a výlet", + "Weather and a trip", + "Too a enough, will a going to v předpovědi. Čtení a doplňování.", + "Too and enough, will and going to in a forecast. Reading and gaps.", + [ + S( + "1 / 2 • Too / enough", + "Příliš a dost", + "Tip: too stojí před přídavným jménem, enough za ním.", + [ + "It's too cold to swim. The water isn't warm enough.", + "Too many people, too much rain.", + "Enough + podstatné jméno: enough water. Nebo za přídavným jménem: old enough.", + "Not enough time je častější než too little time.", + ], + ), + S( + "2 / 2 • Předpověď", + "Co říct, když se díváte z okna", + "Tip: mraky, které vidíte, vedou k going to. Odhad bez důkazu k will.", + [ + "Those clouds are black. It's going to storm.", + "I think it will be sunny on Sunday. To je jen odhad.", + "If it rains, we'll stay in the cabin.", + "Slovník: foggy, chilly, pouring, a heatwave, a breeze.", + ], + ), + ], + reading=( + "We wanted to cycle to the lake, but the morning was too foggy to " + "start. By ten the sun was out and the path was dry enough. There " + "were too many cyclists near the dam, so we took a quieter road. " + "We didn't have enough water, which was a mistake. At the lake a " + "guard said, \"Look at that cloud. It's going to pour. I think the " + "storm will last an hour.\" We were not fast enough. We got soaked." + ), + readq=[ + Q( + "Why didn't they start early?", + ["The bikes were broken", "It was too foggy", "The lake was closed", "They had no map"], + 1, + "too foggy to start.", + ), + Q( + "Why did they change the road?", + [ + "There were too many cyclists", + "They were lost", + "It was pouring", + "The guard sent them back", + ], + 0, + "too many cyclists near the dam.", + ), + Q( + "What mistake did they make?", + ["They forgot the bikes", "They didn't have enough water", "They started at night", "They swam"], + 1, + "didn't have enough water.", + ), + Q( + "What did the guard predict from the cloud?", + ["Fog", "A heatwave", "Rain", "Snow"], + 2, + "It's going to pour.", + ), + ], + gaps=[ + G("The rucksack is ___ heavy to carry.", "too", "Too + přídavné jméno."), + G("She isn't tall ___ to reach the shelf.", "enough", "Enough za přídavným jménem."), + G("There is too ___ sugar in this tea.", "much", "Nepočitatelné: too much."), + G("Look at the sky. It ___ snow.", "is going to|'s going to", "Viditelný důkaz."), + ], + ), + L( + "Festival", + "A festival", + "Předpřítomný čas proti minulému. Poslech a kvíz.", + "Present perfect versus past simple. Listening and a quiz.", + [ + S( + "1 / 2 • Dva časy", + "Kdy je děj uzavřený", + "Tip: jakmile zazní yesterday, last year nebo in 2019, patří minulý čas.", + [ + "I have been to the festival three times. Neříkám kdy.", + "I went in 2023. Tehdy pršelo. To už je uzavřené.", + "Have you ever slept in a tent? — Yes. I slept in one last June.", + "Been to znamená, že už je člověk zpátky. Gone to znamená, že je pořád pryč.", + ], + ), + S( + "2 / 2 • Reportáž", + "Co se děje na místě", + "Tip: reportér míchá „už se stalo“ a „bylo to včera večer“.", + [ + "The gates have just opened.", + "Last night the main band played for two hours.", + "Have you seen the lanterns yet?", + "A local baker has made a cake in the shape of a violin.", + ], + ), + ], + listening=( + "This is Nora at the river festival. The gates have just opened and " + "hundreds of people have already come in. I have never seen so many " + "lanterns. Last night the main band played for two hours, but tonight's " + "singer hasn't arrived yet. A local baker has made a cake in the shape " + "of a violin. I tried a piece yesterday. It was delicious. Have you " + "ever been here? I came for the first time in 2022, and I have come " + "back every summer since then." + ), + listenq=[ + Q( + "What has just happened?", + ["The band has left", "The gates have opened", "It has started to snow", "The baker has closed"], + 1, + "The gates have just opened.", + ), + Q( + "When did the main band play?", + ["Just now", "Last night", "In 2022", "Every summer"], + 1, + "Last night the main band played.", + ), + Q( + "What hasn't happened yet?", + ["People have come in", "Tonight's singer has arrived", "Nora tried the cake", "The gates opened"], + 1, + "hasn't arrived yet.", + ), + Q( + "When did Nora come for the first time?", + ["Yesterday", "Last night", "In 2022", "This morning"], + 2, + "for the first time in 2022.", + ), + ], + quiz=[ + Q( + "I ___ this play in 2019.", + ["have seen", "saw", "seen", "have saw"], + 1, + "Rok uzavírá děj: saw.", + ), + Q( + "She has ___ to Paris. She is still there.", + ["been", "gone", "went", "going"], + 1, + "Pořád je pryč: gone.", + ), + Q( + "___ you ever slept in a tent?", + ["Did", "Have", "Do", "Were"], + 1, + "Ever + present perfect: Have you ever…?", + ), + Q( + "We ___ the lanterns yesterday.", + ["have seen", "saw", "seen", "have watch"], + 1, + "Yesterday → past simple.", + ), + ], + ), + L( + "Kdyby", + "If I could", + "První a druhá podmínková věta. Psaní a doplňování.", + "First and second conditionals. Writing and gaps.", + [ + S( + "1 / 2 • Podmínky", + "Reálná a hypotetická", + "Tip: v if větě není will. If it rains, we will stay in.", + [ + "1. kondicionál: If I finish early, I will call you. Je to možné.", + "2. kondicionál: If I had a car, I would drive. Teď auto nemám.", + "V formální druhé podmínce je were: If I were you, I would apologise.", + "Unless = if not: We'll miss the bus unless we leave now.", + ], + ), + S( + "2 / 2 • Vlastní věta", + "Jedna reálná, jedna vysněná", + "Tip: would se v if větě druhé podmínky neopakuje na obou stranách špatně. If I would have je chyba.", + [ + "Napište situaci, která se může stát tento týden.", + "Pak situaci, která teď pravda není.", + "Would like není druhá podmínka. To je jen přání: I'd like a tea.", + "Krátký text ať má oba typy, ať je rozdíl vidět.", + ], + ), + ], + writing=W( + "Write at least 35 words. Include one real plan with if and will, and one unreal dream with would. Use the word if twice.", + "If I finish my project tonight, I will go climbing with Eva on Saturday. We will take the early bus if the weather is fine. If I had more free time, I would learn the guitar. I would also travel by night train if tickets were cheaper. If is a small word with two very different lives.", + "if|will|would", + 35, + ), + gaps=[ + G("If it ___ (rain), we will stay in the tent.", "rains", "1. podmínka: přítomný čas v if."), + G("If I ___ (be) you, I would tell the truth.", "were|was", "Were je přesnější, was se taky slyší."), + G("She would travel more if she ___ (have) time.", "had", "2. podmínka: minulý tvar."), + G("We'll be late ___ we hurry.", "unless", "Unless = if not."), + ], + ), + L( + "Test 2. ročníku", + "Year 2 test", + "Čtení, poslech, gramatika B1 a krátké psaní.", + "Reading, listening, B1 grammar and a short piece of writing.", + [ + S( + "1 / 2 • B1", + "Co už má držet pohromadě", + "Tip: u předpřítomného času hledejte ever, yet, since. U minulého yesterday a rok.", + [ + "Stupňování, past continuous, modální slovesa rady a povinnosti.", + "Present perfect proti past simple.", + "Who a which, too a enough, první a druhá podmínka.", + "Psaní: krátký souvislý názor, ne odrážkový seznam.", + ], + ), + S( + "2 / 2 • Strategie", + "Jedna věta navíc", + "Tip: v psaní ať je důvod. Because nebo so text zvedne.", + [ + "U čtení škrtněte odpověď, která v textu vůbec není.", + "U poslechu si čísla zapište číslicí.", + "Mustn't není don't have to.", + "If I would have time je chyba. If I had time, I would…", + ], + ), + ], + reading=( + "The night bus to Brno is cheaper than the train, but it is also " + "slower. Last Friday I was sleeping when the driver stopped at a " + "dark petrol station. A woman who had missed her connection asked " + "if she could sit next to me. She has travelled this route since " + "March because her new job starts at six. If the bus is late, she " + "has to run. She says she would move closer if flats were cheaper." + ), + readq=[ + Q( + "The bus is ___ than the train.", + ["faster and cheaper", "cheaper and slower", "the best", "as fast"], + 1, + "cheaper, but slower.", + ), + Q( + "What was the writer doing when the bus stopped?", + ["Reading", "Sleeping", "Running", "Driving"], + 1, + "I was sleeping when the driver stopped.", + ), + Q( + "Why has the woman travelled this route since March?", + ["She likes petrol stations", "Her job starts early", "Flats are cheap", "She missed a concert"], + 1, + "her new job starts at six.", + ), + ], + listening=( + "Doctor: How long have you had this cough? Adam: Since Sunday. I have " + "also had a sore throat for two days. Doctor: You should rest and drink " + "water. You don't have to stay in bed all day, but you mustn't play " + "football this week. If you still feel ill on Friday, come back." + ), + listenq=[ + Q("Since when has Adam had the cough?", ["Since Friday", "Since Sunday", "For a month", "Since March"], 1, "Since Sunday."), + Q("What mustn't he do?", ["Drink water", "Rest", "Play football", "Come back"], 2, "mustn't play football."), + Q("When should he come back?", ["If he is still ill on Friday", "Every day", "Never", "On Sunday morning"], 0, "If you still feel ill on Friday."), + ], + quiz=[ + Q("This hostel is ___ than the hotel.", "cheaper / more cheap / the cheapest / cheap".split(" / "), 0, "cheap → cheaper."), + Q("I have lived here ___ 2020.", ["for", "since", "ago", "during"], 1, "Since + bod v čase."), + Q("You ___ wear a helmet. It's the law.", ["must to", "have to", "don't have", "should to"], 1, "Pravidlo: have to."), + Q("If I ___ richer, I would take the train.", ["am", "will be", "were", "would be"], 2, "2. podmínka: were."), + ], + writing=W( + "Write at least 35 words about a trip or a job you would like. Use would and because.", + "I would like to work in a small bookshop by the station because I love quiet mornings. I would talk to customers and I would also read in the breaks. If I had the job, I would cycle there. I have never worked in a shop, but I have helped at school events.", + "would|because", + 35, + ), + ), + ], + }, +] + + +def y3(): + forest = ( + "Last month our class recorded a podcast called The Last Beech. " + "A ranger said that the trees were thirsty because the summers had " + "become hotter. She told us that volunteers had planted two thousand " + "young trees, but she added that many would die unless it rained. " + "If the town banned cars from the forest road, the air would be cleaner. " + "If people keep using it as a shortcut, the soil will wash away." + ) + return { + "sub_cs": "Společnost, prostředí, média a složitější gramatika. Úroveň B1 až B2.", + "sub_en": "Society, the environment, the media and harder grammar. Level B1 to B2.", + "lessons": [ + L( + "Jak se to vyrábí", + "How things are made", + "Trpný rod v přítomnosti a minulosti. Doplňování a kvíz.", + "The present and past passive. Gaps and a quiz.", + [ + S( + "1 / 2 • Passive", + "Když je důležitější děj než původce", + "Tip: be + příčestí. Čas nese sloveso be, ne příčestí.", + [ + "English is spoken here. The window was broken last night.", + "Původce s by, jen když na něm záleží: The book was written by a nurse.", + "Modální trpný rod: The form must be signed. It can be recycled.", + "Nelze trpně vyjádřit každé sloveso. Happen trpný rod nemá.", + ], + ), + S( + "2 / 2 • Popis procesu", + "Od suroviny k věci", + "Tip: first, then, after that, finally. Původce často chybí, protože je obecný.", + [ + "The beans are picked by hand. They are dried in the sun.", + "The jars were labelled yesterday.", + "Waste paper is collected every Thursday.", + "A new bridge is being built. To už je průběhový trpný rod, zatím stačí ho poznat.", + ], + ), + ], + gaps=[ + G("This bread ___ (bake) every morning.", "is baked", "Přítomný trpný rod."), + G("The letters ___ (send) yesterday.", "were sent", "Minulý trpný rod, množné číslo."), + G("The poem ___ (write) by a student.", "was written", "Write → written."), + G("These bottles can ___ (recycle).", "be recycled", "Modální sloveso + be + příčestí."), + G("The window ___ (not break) by the wind. Someone hit it.", "wasn't broken|was not broken", "Zápor minulého trpného rodu."), + ], + quiz=[ + Q( + "Which sentence is passive?", + [ + "Someone stole my bike.", + "My bike was stolen.", + "My bike stole.", + "Someone was steal my bike.", + ], + 1, + "was + stolen.", + ), + Q( + "The news ___ at ten.", + ["is reading", "are read", "is read", "read"], + 2, + "News je jednotné: is read.", + ), + Q( + "You ___ wear a badge. The factory requires it, in the passive of must.", + ["must be worn a badge", "A badge must be worn", "A badge must worn", "A badge is must wear"], + 1, + "A badge must be worn.", + ), + Q( + "They built the school in 1998. → The school ___ in 1998.", + ["built", "was built", "was build", "has built"], + 1, + "was built.", + ), + ], + ), + L( + "Škola, o které se mluví", + "A school people talk about", + "Nepřímá řeč oznamovací. Čtení a kvíz.", + "Reported statements. Reading and a quiz.", + [ + S( + "1 / 2 • Reported speech", + "Posun času", + "Tip: said that, told me that. Tell potřebuje osobu, say ne.", + [ + "„I am tired.“ → She said she was tired.", + "„I will help.“ → He said he would help.", + "„I have finished.“ → She told me she had finished.", + "„I can't come.“ → He said he couldn't come.", + ], + ), + S( + "2 / 2 • Kdy čas neposouvat", + "Pořád to platí", + "Tip: obecné pravdy a situace, které trvají, často zůstávají v přítomném čase.", + [ + "The teacher said that water boils at 100 degrees.", + "She said she lives in Kolín, a pořád tam bydlí.", + "Zájmena se mění podle smyslu: „my book“ → he said his book.", + "Today → that day, tomorrow → the next day, yesterday → the day before. V testu to bude jen u jasných vět.", + ], + ), + ], + reading=( + "Our head teacher told the local paper that the school was changing. " + "She said that students would start an hour later next term. One pupil " + "told the reporter that he was pleased because he hated early buses. " + "A parent said that she was worried. She said her daughter had joined " + "three clubs and had very little time. The cook said that lunch would " + "still be at noon. He added that the menu was going to include more vegetables." + ), + readq=[ + Q( + "What did the head teacher say about the start of school?", + [ + "It would start an hour later", + "It would start at night", + "It had already closed", + "Clubs would end", + ], + 0, + "students would start an hour later.", + ), + Q( + "Why was the pupil pleased?", + ["He liked buses", "He hated early buses", "He wanted less lunch", "He had three clubs"], + 1, + "because he hated early buses.", + ), + Q( + "What had the daughter done?", + ["She had left school", "She had joined three clubs", "She had become a cook", "She had bought a bus"], + 1, + "had joined three clubs.", + ), + Q( + "Lunch, according to the cook, …", + ["would move to the evening", "would still be at noon", "had been cancelled", "was only vegetables"], + 1, + "would still be at noon.", + ), + ], + quiz=[ + Q( + "„I am hungry.“ → She said she ___ hungry.", + ["is", "was", "has", "were be"], + 1, + "am → was.", + ), + Q( + "„I will call.“ → He said he ___ call.", + ["will", "would", "called", "has"], + 1, + "will → would.", + ), + Q( + "Which verb needs a person?", + ["say", "tell", "speak that", "talk that"], + 1, + "tell somebody.", + ), + Q( + "She said, „I can't swim.“ → She said she ___.", + ["can't swim", "couldn't swim", "doesn't swim", "hasn't swum"], + 1, + "can → could. V nepřímé řeči o minulém rozhovoru.", + ), + ], + ), + L( + "Poslední buk", + "The last beech", + "Podmínky a slovník prostředí. Poslech podcastu a doplňování.", + "Conditionals and environment vocabulary. A podcast and gaps.", + [ + S( + "1 / 2 • Prostředí", + "Konkrétní slova, ne jen nature", + "Tip: drought, soil, volunteer, ban, shortcut. U maturitního tématu zabírají víc než slovo pollution samo.", + [ + "A drought is a long time without rain. Soil is the earth plants grow in.", + "Unless it rains, the young trees will die.", + "If the town banned cars, the air would be cleaner. Teď je nezakázalo, proto would.", + "If people keep using the road, the soil will wash away. To se může stát.", + ], + ), + S( + "2 / 2 • Podcast", + "Kdo co řekl", + "Tip: u nepřímé řeči v poslechu si pamatujte sloveso řekl/řekla a pak obsah.", + [ + "Ranger = strážce lesa.", + "Young trees were planted, but many may still die.", + "A shortcut šetří čas a někdy ničí místo.", + "Čísla si zapište: two thousand.", + ], + ), + ], + listening=forest, + listenq=[ + Q( + "What did the ranger say about the trees?", + ["They were thirsty", "They had been cut down", "They were a shortcut", "They banned cars"], + 0, + "the trees were thirsty.", + ), + Q( + "How many young trees had volunteers planted?", + ["Two hundred", "Two thousand", "Twenty", "None"], + 1, + "two thousand.", + ), + Q( + "The young trees will die unless…", + ["people drive there", "it rains", "the podcast ends", "the soil is sold"], + 1, + "unless it rained / unless it rains. V textu: unless it rained v nepřímé řeči.", + ), + Q( + "What would make the air cleaner?", + [ + "Banning cars from the forest road", + "Using the road as a shortcut", + "Cutting the beeches", + "Washing the soil", + ], + 0, + "If the town banned cars.", + ), + ], + gaps=[ + G("If we ___ (ban) cars here, the air would be cleaner.", "banned", "2. podmínka."), + G("The soil will wash away ___ people stop using the shortcut.", "unless", "Unless = if not. Pozor: věta chce if not ve významu dokud nepřestanou. Unless people stop = if they don't stop."), + G("Many young trees ___ (plant) last month.", "were planted", "Minulý trpný rod."), + G("She said the summers ___ (become) hotter.", "had become", "Posun předpřítomného času."), + ], + ), + L( + "Pro a proti", + "For and against", + "Stavba úvahy a spojovací výrazy. Esej a kvíz.", + "Essay structure and linking words. An essay and a quiz.", + [ + S( + "1 / 2 • Čtyři kroky", + "Úvod, dvě strany, závěr", + "Tip: v úvodu otázku neopakujte doslova. Převeďte ji vlastními slovy.", + [ + "Úvod: o čem text je a že existují dva pohledy.", + "Jeden odstavec pro výhody, jeden pro nevýhody.", + "On the one hand … On the other hand …", + "Závěr: váš názor. In my opinion, I believe. Nepřidávejte úplně nový argument.", + ], + ), + S( + "2 / 2 • Spojky", + "However není because", + "Tip: however stojí mezi větami a odděluje se čárkou. Although spojuje věty v jednu.", + [ + "School uniforms are cheaper. However, they limit personality.", + "Although uniforms save time, many students hate them.", + "Because dává důvod. So dává výsledek.", + "In conclusion shrnuje. Nepíše se In the conclusion.", + ], + ), + ], + writing=W( + "Write a for-and-against paragraph of at least 50 words about school uniforms. Use however and because, and finish with your opinion using believe.", + "School uniforms are a common argument. They are practical because students do not have to decide what to wear, and families spend less money. However, a uniform hides personality and can feel childish at eighteen. Although it makes mornings faster, it does not make lessons better. I believe schools should keep a simple dress code, but they should not force one jacket on everyone.", + "however|because|believe", + 50, + ), + quiz=[ + Q( + "Which linker shows contrast?", + ["because", "however", "so", "for example"], + 1, + "However staví protiklad.", + ), + Q( + "___ uniforms save time, students still complain.", + ["However", "Although", "So", "Believe"], + 1, + "Although spojuje dvě části jedné věty.", + ), + Q( + "Choose the best ending of an essay.", + [ + "In conclusion, I have no idea.", + "In conclusion, I believe a simple rule is enough.", + "Because however so.", + "To begin with, the essay starts now.", + ], + 1, + "Závěr má názor, ne nový zmatek.", + ), + Q( + "Because introduces…", + ["a result only", "a reason", "a list of names", "a question"], + 1, + "Because = důvod.", + ), + ], + ), + L( + "Kdo to musel být", + "It must have been", + "Modální slovesa dedukce. Čtení záhady a kvíz.", + "Modals of deduction. A short mystery and a quiz.", + [ + S( + "1 / 2 • Dedukce", + "Jisté, možné, vyloučené", + "Tip: must je skoro jistota, might je možnost, can't je „to přece nejde“.", + [ + "The lights are on. She must be at home.", + "He might be in the lab. I'm not sure.", + "That can't be Eva. Eva is in Vienna this week.", + "O minulosti: must have left, might have forgotten, can't have seen it.", + ], + ), + S( + "2 / 2 • Vynález", + "Čtěte důkazy, ne nadpis", + "Tip: u must have hledejte důkaz, který už nejde rozumně popřít.", + [ + "A notebook with today's date is stronger than a guess.", + "Can't have znamená, že to odporuje faktu.", + "Might have nechává dvě možnosti otevřené.", + "Nepoužívejte can pro dedukci v kladné větě. Can umí, must musí být.", + ], + ), + ], + reading=( + "Someone left a strange machine in the physics lab. It was warm, so " + "it must have been switched on recently. The only key can't have been " + "used by the caretaker, because he is in hospital. Two students, Nora " + "and Adam, know the code. Nora might have come back after the club, " + "but her train ticket shows she was in Pardubice. Adam's notebook is " + "on the bench and the handwriting matches the label. He must have built it." + ), + readq=[ + Q( + "Why must the machine have been on recently?", + ["It was noisy", "It was warm", "It was new", "It was labelled"], + 1, + "It was warm.", + ), + Q( + "Why can't the caretaker have used the key?", + ["He lost it", "He is in hospital", "He hates physics", "He was in Pardubice"], + 1, + "he is in hospital.", + ), + Q( + "Where was Nora?", + ["In the lab", "In Pardubice", "In hospital", "On the bench"], + 1, + "her ticket shows Pardubice.", + ), + Q( + "Who must have built the machine?", + ["The caretaker", "Nora", "Adam", "Nobody"], + 2, + "The notebook and the handwriting point to Adam.", + ), + ], + quiz=[ + Q("The window is broken and his bag is gone. He ___ left in a hurry.", ["must have", "must", "can have", "should"], 0, "Minulá skoro jistota: must have left."), + Q("That ___ be Lea. Lea is abroad.", ["must", "can't", "should", "is"], 1, "Odporuje faktu: can't."), + Q("I'm not sure. They ___ have missed the bus.", ["must", "can't", "might", "are"], 2, "Možnost: might have."), + Q("Which sentence is a deduction, not a duty?", ["You must wear gloves.", "She must be tired.", "You must sign this.", "Visitors must wait."], 1, "She must be tired je úsudek."), + ], + ), + L( + "Zprávy dne", + "The news desk", + "Nepřímé otázky. Poslech rozhovoru a kvíz.", + "Reported questions. An interview and a quiz.", + [ + S( + "1 / 2 • Reported questions", + "Otázka se v nepřímé řeči narovná", + "Tip: žádný otazník a pořadí podmětu a slovesa jako v oznamovací větě.", + [ + "„Where do you live?“ → She asked me where I lived.", + "„Did you see it?“ → He asked if I had seen it.", + "Whether a if jsou u zjišťovacích otázek obě možné.", + "Tázací výraz zůstává: what, where, why, how long.", + ], + ), + S( + "2 / 2 • Redakce", + "Novinář se ptá starosty", + "Tip: v poslechu odlište otázku reportéra od odpovědi starosty.", + [ + "Asked if the bridge was safe.", + "Wondered how long the repairs would take.", + "The mayor said he didn't know yet.", + "Slovesa ask, wonder, want to know.", + ], + ), + ], + listening=( + "Reporter: Mayor, is the old bridge safe? Mayor: I can't promise that. " + "An engineer told me that two cables were weak. Reporter: How long will " + "the repairs take? Mayor: She asked the firm the same question yesterday. " + "They said they didn't know yet. Reporter: Did anyone complain? Mayor: " + "A shopkeeper asked me why the bus stop had moved. I asked her if she " + "could wait until Friday. Reporter: Will you close the bridge? Mayor: " + "We might. I have asked the police whether they can send officers tonight." + ), + listenq=[ + Q( + "What did the engineer tell the mayor?", + ["The bridge was new", "Two cables were weak", "The shop was closed", "Friday was impossible"], + 1, + "two cables were weak.", + ), + Q( + "What didn't the firm know?", + ["The mayor's name", "How long the repairs would take", "Where the bus stop was", "Who the police were"], + 1, + "They said they didn't know yet. Šlo o délku oprav.", + ), + Q( + "What did the shopkeeper ask?", + [ + "Why the bus stop had moved", + "If the mayor could swim", + "Where the cables were made", + "Whether Friday was a holiday", + ], + 0, + "why the bus stop had moved.", + ), + Q( + "Who might come tonight?", + ["The firm", "The shopkeeper", "Police officers", "The engineer only"], + 2, + "whether they can send officers tonight.", + ), + ], + quiz=[ + Q( + "„Where do you live?“ → He asked me where I ___.", + ["did live", "lived", "do live", "living"], + 1, + "Pořadí oznamovací věty: where I lived.", + ), + Q( + "„Are you ready?“ → She asked if I ___.", + ["was ready", "am I ready", "ready was", "did ready"], + 0, + "if I was ready.", + ), + Q( + "Which sentence is wrong?", + [ + "He asked where she was.", + "He asked where was she.", + "He asked if she was tired.", + "He wanted to know why she had left.", + ], + 1, + "V nepřímé otázce není inverze.", + ), + Q( + "Whether means about the same as…", + ["because", "if", "however", "whose"], + 1, + "Whether ≈ if v nepřímé zjišťovací otázce.", + ), + ], + ), + L( + "Frázová slovesa v příběhu", + "Phrasal verbs in a story", + "Běžná frázová slovesa. Doplňování a kvíz.", + "Common phrasal verbs. Gaps and a quiz.", + [ + S( + "1 / 2 • Částice mění význam", + "Look není look after", + "Tip: frázové sloveso se učte jako jednu jednotku, s českým protějškem ve větě.", + [ + "look after = starat se, look for = hledat, look forward to = těšit se.", + "give up = vzdát se, find out = zjistit, run out of = dojít zásoba.", + "turn off = vypnout, take off = sundat nebo vzlétnout, get on = nastoupit i vycházet.", + "Look forward to je následované -ing: I look forward to seeing you.", + ], + ), + S( + "2 / 2 • Příběh", + "Jedno sloveso, jedna situace", + "Tip: když částici neznáte, představte si gesto. Off je pryč, up je do konce.", + [ + "The bus took off není o oblečení. U letadla ano, u autobusu spíš pulled away.", + "Take off a coat = sundat. Take off = vzlétnout. Kontext rozhoduje.", + "Get on with somebody = vycházet s někým.", + "Run out of time je u testu častější než run out time.", + ], + ), + ], + gaps=[ + G("Can you look ___ my dog this weekend?", "after", "Look after = postarat se."), + G("We are looking ___ to the trip.", "forward", "Look forward to."), + G("Don't give ___. The last chapter is the best.", "up", "Give up = vzdát to."), + G("I need to find ___ who left the note.", "out", "Find out = zjistit."), + G("We've run ___ of milk.", "out", "Run out of."), + ], + quiz=[ + Q("Please turn ___ the lights.", ["off", "after", "forward", "out of"], 0, "Turn off = vypnout."), + Q("She gets ___ well with her neighbours.", ["on", "off", "up", "after"], 0, "Get on with = vycházet."), + Q("He took ___ his wet coat.", ["off", "up to", "after", "forward"], 0, "Sundat: take off."), + Q("I look forward to ___ you.", ["see", "seeing", "saw", "seen"], 1, "To je předložka, následuje -ing."), + ], + ), + L( + "Test 3. ročníku", + "Year 3 test", + "Čtení, poslech, gramatika B1+ a krátká úvaha.", + "Reading, listening, B1+ grammar and a short argument.", + [ + S( + "1 / 2 • B1+", + "Trpný rod, řeč, podmínky, dedukce", + "Tip: u trpného rodu hledejte be. U nepřímé otázky žádnou inverzi.", + [ + "was written, must be signed, is spoken.", + "said she would, asked where I lived.", + "If they banned cars, the air would be cleaner.", + "must have, might have, can't have.", + ], + ), + S( + "2 / 2 • Úvaha ve zkratce", + "However a because v jednom textu", + "Tip: jedna výhoda, jeden háček, jedna věta s názorem.", + [ + "Neopakujte zadání první větou.", + "Phrasal verbs nechte ve správné částici. Look after není look for.", + "Told me, ne said me.", + "In conclusion je až na konci, ne v úvodu.", + ], + ), + ], + reading=( + "A repair café opened in a former bakery. Broken radios are mended " + "there on Saturdays. The founder said that people would rather repair " + "than buy if they were shown how. She told a reporter that twenty " + "kettles had been saved in March. Visitors have to bring the broken " + "object, but they don't have to pay. If the café closed, a lot of " + "working parts would be thrown away." + ), + readq=[ + Q("What happens to radios there?", ["They are sold", "They are mended", "They are banned", "They are thrown away at once"], 1, "are mended."), + Q("How many kettles had been saved in March?", ["Twelve", "Twenty", "Two thousand", "None"], 1, "twenty kettles."), + Q("What would happen if the café closed?", ["Parts would be thrown away", "Radios would be cheaper", "People would pay more", "The bakery would reopen as a shop"], 0, "working parts would be thrown away."), + ], + listening=( + "Editor: Did the mayor answer you? Journalist: I asked him whether the " + "bridge was safe. He said he didn't know. I also asked how long the " + "repairs would take. He told me that the firm hadn't decided. A resident " + "asked me why the paper hadn't printed a map. I said we would print one " + "tomorrow." + ), + listenq=[ + Q("What did the journalist ask about safety?", ["Whether the bridge was safe", "If the paper was closed", "Where the mayor lived", "Why kettles break"], 0, "whether the bridge was safe."), + Q("What hadn't the firm done?", ["Decided", "Printed a map", "Closed the paper", "Asked the resident"], 0, "hadn't decided."), + Q("What did the journalist promise?", ["To close the bridge", "To print a map tomorrow", "To repair the cables", "To interview a baker"], 1, "we would print one tomorrow."), + ], + quiz=[ + Q("The cake ___ by a student.", ["baked", "was baked", "was bake", "has bake"], 1, "was baked."), + Q("She asked me where I ___.", ["did live", "lived", "do live", "living"], 1, "where I lived."), + Q("He ___ be at school. I just saw him there.", ["can't", "must", "mustn't", "should to"], 1, "Skoro jistota: must."), + Q("Please look ___ the tickets. I can't find them.", ["for", "forward", "after", "up of"], 0, "Look for = hledat. Look after by bylo hlídat."), + ], + writing=W( + "In at least 40 words, give one reason for repairing things and one objection. Use because and however.", + "Repairing things matters because a kettle can work for years after a small fix. However, some people do not have the tools or the time, so a café where volunteers help is a fair idea. I believe every town should have one quiet room for broken objects. It is slower than shopping, but it is kinder.", + "because|however", + 40, + ), + ), + ], + } + + +def y4(): + article = ( + "When the night train from Prague to Berlin returned last spring, " + "stations filled with people who prefer a bed to a boarding gate. " + "A single ticket is often more expensive than a flight, which surprises " + "anyone who thinks trains are always the cheap choice. Still, passengers " + "say they arrive calmer. They can read, talk and avoid the bright shops " + "of an airport. Critics point out that a delay of two hours is common " + "and that the cabins are small. The railway has promised that new carriages " + "will be added if demand stays high. A student I spoke to had taken the " + "train three times. She said she would fly only if she missed the evening " + "departure. For her, the journey is part of the trip, not a problem to be " + "deleted." + ) + interview = ( + "Interviewer: Why did you apply to the summer camp in Wales? " + "Hana: I have worked with children for two years, and I wanted a job " + "where English is the only language. Interviewer: What would you do if " + "a child refused to join an activity? Hana: I wouldn't force them. I " + "would ask why, and I might offer a quieter task. Interviewer: Have you " + "ever dealt with an emergency? Hana: Yes. Last August a boy cut his foot. " + "I cleaned the cut and called the nurse. I didn't give him any medicine. " + "Interviewer: The camp is rainy. Is that a problem? Hana: Not really. " + "I have already bought boots. I look forward to the hills, although I " + "know the work will be tiring." + ) + return { + "sub_cs": "Maturitní dovednosti: mluvení, formální psaní, čtení, poslech a gramatika. Úroveň B2.", + "sub_en": "Exam skills: speaking, formal writing, reading, listening and grammar. Level B2.", + "lessons": [ + L( + "Maturitní kartička", + "The speaking card", + "Strategie ústní zkoušky a kolokace osobních témat. Kvíz a doplňování.", + "Speaking-exam strategy and personal-topic collocations. A quiz and gaps.", + [ + S( + "1 / 2 • Ústní zkouška", + "Čtyři minuty nejsou román", + "Tip: katalog CERMAT chce souvislý projev, popis obrázku, porovnání a reakci na otázky. Ne seznam slov.", + [ + "Úvod jednou větou: I'd like to talk about my town, which is smaller than it looks.", + "Obecné plus osobní: Many people commute. I cycle, because the centre is close.", + "Když slovo chybí, opis: the thing you use to open a tin.", + "Na obrázku: In the foreground, in the background, it looks as if.", + ], + ), + S( + "2 / 2 • Kolokace", + "Make, do, have, take", + "Tip: české „dělat“ se rozpadá. Do homework, make a mistake, take a photo, have a rest.", + [ + "Make a decision, make friends, make the bed.", + "Do the washing-up, do a course, do someone a favour.", + "Take part in, take care of, take an exam.", + "Have a look, have a word with, have trouble with.", + ], + ), + ], + gaps=[ + G("I need to ___ a decision before Friday.", "make", "Make a decision."), + G("She ___ her homework on the train.", "does|did", "Do homework."), + G("Can I ___ a photo of the bridge?", "take", "Take a photo."), + G("Let's ___ a rest. We've walked for hours.", "have", "Have a rest."), + G("He ___ part in the school play every year.", "takes|took", "Take part in."), + ], + quiz=[ + Q( + "You don't know the word kettle. What helps?", + [ + "Stop the exam", + "Paraphrase it", + "Switch to Czech for the rest", + "Invent a Latin word and insist", + ], + 1, + "Opis je lepší než ticho.", + ), + Q( + "In the foreground means…", + ["at the back", "at the front of the picture", "outside the frame", "in the past"], + 1, + "Foreground = popředí.", + ), + Q( + "Which collocation is right?", + ["do a mistake", "make a mistake", "take a mistake", "have a mistake"], + 1, + "Make a mistake.", + ), + Q( + "A strong speaking answer…", + [ + "is a list of single words", + "adds a personal example", + "translates the question", + "avoids all verbs", + ], + 1, + "Obecné plus vlastní příklad.", + ), + ], + ), + L( + "Formální e-mail", + "A formal email", + "Žádost a stížnost. Psaní a kvíz rejstříku.", + "A request and a complaint. Writing and a register quiz.", + [ + S( + "1 / 2 • Kostra", + "Komu, proč, co chcete, jak se loučíte", + "Tip: neznámé jméno → Dear Sir or Madam a Yours faithfully. Známé jméno → Dear Ms Novák a Yours sincerely.", + [ + "I am writing to apply for… / to complain about…", + "I would be grateful if you could…", + "Kontrakce v čistě formálním textu omezte: I am, not I'm.", + "Na konci celé jméno. Žádné Cheers ani xoxo.", + ], + ), + S( + "2 / 2 • Stížnost", + "Fakta, dopad, žádost", + "Tip: zlost text oslabuje. Datum, číslo spoje a klidná žádost ho posílí.", + [ + "The train on 3 May, which I had booked, arrived 90 minutes late.", + "As a result I missed the interview.", + "I would like a refund or an explanation.", + "I look forward to your reply.", + ], + ), + ], + writing=W( + "Write a formal email of at least 55 words to a railway company. Complain about a delay. Start with Dear, use would, and end with sincerely or faithfully.", + "Dear Sir or Madam, I am writing to complain about the night train to Berlin on 3 May. It arrived ninety minutes late, so I missed a morning interview. I would be grateful if you could explain the delay and offer a refund. I have attached my ticket. Please reply to this address. Yours faithfully, Tereza Malá.", + "dear|would|faithfully", + 55, + ), + quiz=[ + Q( + "You do not know the person's name. You end with…", + ["Yours faithfully", "Yours sincerely", "Cheers", "Love"], + 0, + "Neznámé jméno: faithfully. Po Dear Sir or Madam.", + ), + Q( + "Dear Ms Novák, … ___", + ["Yours faithfully", "Yours sincerely", "Hi again", "Bye"], + 1, + "Jméno znáte: sincerely.", + ), + Q( + "Which opening is formal?", + ["Hey!!!", "I am writing to apply for the post.", "I wanna the job.", "What's up"], + 1, + "I am writing to…", + ), + Q( + "A formal request sounds like…", + [ + "Give me a refund now.", + "I would be grateful if you could look into this.", + "You guys messed up.", + "Send money pls.", + ], + 1, + "I would be grateful if you could…", + ), + ], + ), + L( + "Noční vlak", + "The night train", + "Delší čtení ve stylu didaktického testu. Čtyři otázky k článku.", + "A longer reading in the style of the written paper. Four questions.", + [ + S( + "1 / 2 • Čtení u maturity", + "Nečtěte každé slovo stejně pomalu", + "Tip: u výběru ze čtyř možností nejdřív zjistěte, na co se otázka ptá, a teprve pak hledejte odstavec.", + [ + "Hlavní myšlenka bývá v úvodu nebo v závěru, ne v jednom příkladu.", + "Distraktor často opakuje slovo z textu, ale v jiném vztahu.", + "Which surprises… uvádí postoj, ne jen fakt o ceně.", + "Not given v tomhle cvičení není. Jedna možnost je opřená o text.", + ], + ), + S( + "2 / 2 • Slovník z kontextu", + "Boarding gate a carriage", + "Tip: neznámé slovo ohraničte větou před ním a za ním.", + [ + "A boarding gate je letištní východ k letadlu. Text ho staví proti posteli ve vlaku.", + "A carriage je vůz vlaku.", + "Demand = poptávka. If demand stays high, přidají vozy.", + "Deleted journey by byl let, který člověk chce mít co nejkratší.", + ], + ), + ], + reading=article, + readq=[ + Q( + "What is the main point?", + [ + "Flights have been cancelled", + "Some people choose the night train even when it costs more", + "Berlin has closed its station", + "Students may not travel", + ], + 1, + "Lístek bývá dražší než let, a přesto lidé vlak volí.", + ), + Q( + "What surprises some readers?", + [ + "That a train ticket can cost more than a flight", + "That stations are empty", + "That nobody reads", + "That the train is free", + ], + 0, + "more expensive than a flight, which surprises…", + ), + Q( + "What do critics mention?", + ["Free meals", "Delays and small cabins", "Too many books", "A lack of stations"], + 1, + "a delay of two hours, cabins are small.", + ), + Q( + "The student would fly only if…", + [ + "the ticket was cheap", + "she missed the evening departure", + "the cabins were small", + "she hated reading", + ], + 1, + "only if she missed the evening departure.", + ), + Q( + "New carriages will be added if…", + ["critics agree", "demand stays high", "flights are banned", "students complain"], + 1, + "if demand stays high.", + ), + ], + ), + L( + "Tábor ve Walesu", + "A camp in Wales", + "Delší poslech: pohovor. Pět otázek.", + "A longer listening: an interview. Five questions.", + [ + S( + "1 / 2 • Poslech u maturity", + "Dvě přehrání stačí, když víte co", + "Tip: při prvním poslechu chyťte kostru, při druhém doplňte past, podmínku a zápor.", + [ + "Otázky si přečtěte dřív. Podtrhněte tázací slovo.", + "Čísla a never/already mění význam celé věty.", + "Wouldn't force není couldn't.", + "I didn't give him any medicine je důležité právě proto, že je zápor.", + ], + ), + S( + "2 / 2 • Práce s dětmi", + "Slovník pohovoru", + "Tip: deal with an emergency, refuse, offer, look forward to.", + [ + "For two years je délka, ne bod v čase.", + "A quieter task je alternativa, ne trest.", + "Although přiznává nevýhodu a názor nemění.", + "Boots jsou důkaz, že s deštěm počítá. Proto going to / already.", + ], + ), + ], + listening=interview, + listenq=[ + Q( + "Why does Hana want the job?", + [ + "She has never met children", + "She wants a job where only English is spoken", + "She hates hills", + "She wants to sell boots", + ], + 1, + "where English is the only language.", + ), + Q( + "How long has she worked with children?", + ["For two weeks", "For two years", "Since last August only", "She hasn't"], + 1, + "for two years.", + ), + Q( + "What would she do if a child refused an activity?", + [ + "Force them", + "Send them home", + "Ask why and maybe offer a quieter task", + "Give them medicine", + ], + 2, + "I wouldn't force them. I might offer a quieter task.", + ), + Q( + "What didn't she do in the emergency?", + ["Clean the cut", "Call the nurse", "Give medicine", "Help the boy"], + 2, + "I didn't give him any medicine.", + ), + Q( + "How does she feel about the rain?", + [ + "She has already bought boots and still looks forward to the hills", + "She will refuse the job", + "She has never seen rain", + "She hates children", + ], + 0, + "already bought boots, look forward to the hills.", + ), + ], + ), + L( + "Názorová esej", + "An opinion essay", + "Struktura názoru. Esej a kvíz o odstavcích.", + "Opinion-essay structure. An essay and a quiz about paragraphs.", + [ + S( + "1 / 2 • Názor", + "Nejdřív stanovisko, pak důvody", + "Tip: u názorové eseje čtenář nemá hádat, na čí straně jste. Řekněte to v úvodu.", + [ + "I believe that… / In my opinion…", + "First reason, second reason, a short answer to the other side.", + "Although some people argue that…, I still think…", + "Závěr ať zní jako závěr, ne jako třetí nový důvod.", + ], + ), + S( + "2 / 2 • Rozsah", + "Radši čtyři hutné věty než deset prázdných", + "Tip: u písemné práce se počítají slova. Opakování zadání se nepočítá jako myšlenka.", + [ + "Každý důvod ať má příklad z běžného života.", + "Nepoužívejte etc. Místo toho jedno konkrétní slovo.", + "Spojky: firstly, in addition, on the other hand, in conclusion.", + "Kontraktované tvary jsou v názorové eseji v pořádku, když text není stížnost úřadu.", + ], + ), + ], + writing=W( + "Write an opinion of at least 60 words: should students have a gap year before university? Use believe, although and in conclusion.", + "I believe a gap year can help, but only when it has a plan. Although some students waste the time, others work, travel or look after a relative and return calmer. A year in a job also shows whether a degree is really necessary. On the other hand, a long break can make it harder to study again. In conclusion, a gap year is useful if the person can explain what they will do with it.", + "believe|although|conclusion", + 60, + ), + quiz=[ + Q( + "Where should your opinion appear in this kind of essay?", + ["Only in a footnote", "In the introduction and again at the end", "Never", "Only in the title"], + 1, + "Úvod i závěr.", + ), + Q( + "Although introduces…", + ["a concession", "a list of books", "a greeting", "a date"], + 0, + "Ústupka: something true that does not win.", + ), + Q( + "In conclusion should…", + ["start a brand-new topic", "sum up", "replace the introduction", "ask the examiner a question"], + 1, + "Shrnutí.", + ), + Q( + "Which is the best topic sentence?", + [ + "Gap year gap year gap year.", + "A gap year is worth it when it has a clear plan.", + "Hello my name is and I will write.", + "Etc. etc. etc.", + ], + 1, + "Věta, která už nese názor.", + ), + ], + ), + L( + "Slovní zásoba okruhů", + "Topic vocabulary", + "Kolokace k maturitním tématům. Doplňování a kvíz.", + "Collocations for the exam topics. Gaps and a quiz.", + [ + S( + "1 / 2 • Okruhy", + "Ne izolovaná slovíčka", + "Tip: ústní zkouška sahá na osobu, rodinu, domov, školu, volný čas, kulturu, sport, cestování, zdraví, jídlo, služby, práci, společnost a přírodu.", + [ + "Heavy rain, strong coffee, fast food, junk food. Ne strong rain.", + "A balanced diet, processed food, a food allergy.", + "Public transport, rush hour, a traffic jam, a return ticket.", + "Renewable energy, a carbon footprint, a heatwave, a drought.", + ], + ), + S( + "2 / 2 • Společnost a práce", + "Fráze, které unesou odstavec", + "Tip: apply for a job, hand in a notice, earn a living, from home.", + [ + "A gap year, an apprenticeship, a degree, tuition fees.", + "Freedom of speech, a stereotype, volunteer work.", + "Break the law, pay a fine, a witness.", + "Mental health, a check-up, be in pain. Ne have pain in the leg jako jediná možnost: My leg hurts.", + ], + ), + ], + gaps=[ + G("There was heavy ___ all night.", "rain", "Heavy rain, ne strong rain."), + G("I'd like a ___ ticket to Brno.", "return", "Return ticket = zpáteční."), + G("She applied ___ the post of guide.", "for", "Apply for."), + G("Solar power is ___ energy.", "renewable", "Renewable energy."), + G("He has a nut ___, so he can't eat the cake.", "allergy", "A food allergy."), + ], + quiz=[ + Q("Which pair is right?", ["strong rain / heavy coffee", "heavy rain / strong coffee", "fast coffee / junk rain", "a traffic diet"], 1, "Heavy rain, strong coffee."), + Q("Rush hour is…", ["a quiet morning", "the busiest time to travel", "a kind of ticket", "a school subject"], 1, "Špička."), + Q("An apprenticeship is…", ["a fine", "training for a job", "a heatwave", "a stereotype"], 1, "Praktická příprava na řemeslo nebo práci."), + Q("My leg ___.", ["hurts", "is pain", "has hurted", "does hurtness"], 0, "My leg hurts."), + ], + ), + L( + "Gramatická ambulance", + "Grammar clinic", + "Smíšené jevy k didaktickému testu. Kvíz a doplňování.", + "Mixed points for the language paper. A quiz and gaps.", + [ + S( + "1 / 2 • Články a časy", + "To, co se v testu sype", + "Tip: the night train, a night train, night trains. Člen podle toho, jestli čtenář ví který.", + [ + "I have lived here since May. I lived there in 2018.", + "If I were you, I would wait. If it rains, we will wait.", + "The book was written in 1962. People speak English here → English is spoken here.", + "Used to: I used to walk. Zvyk, který už neplatí. Didn't use to.", + ], + ), + S( + "2 / 2 • Věta", + "Slovosled a shoda", + "Tip: never dávejte před sloveso, ale za am/is/are: She is never late. She never arrives late.", + [ + "A little time, a few minutes. Much v záporu a otázce, a lot of v kladné větě je bezpečné.", + "Suggest + -ing nebo suggest that. Suggest me to go je chyba.", + "Enjoy, avoid, mind, can't help + -ing.", + "Want, decide, promise, hope + to.", + ], + ), + ], + gaps=[ + G("I ___ (live) in this flat since May.", "have lived|'ve lived", "Since → present perfect."), + G("She suggested ___ (take) the night train.", "taking", "Suggest + -ing."), + G("If I ___ you, I would book earlier.", "were|was", "If I were you."), + G("These letters must ___ (sign) today.", "be signed", "Must be + příčestí."), + G("I ___ to get up at five. I don't anymore.", "used", "Used to."), + ], + quiz=[ + Q("She enjoys ___.", ["to swim", "swimming", "swim", "swam"], 1, "Enjoy + -ing."), + Q("I have ___ finished.", ["yet", "just", "since", "ago"], 1, "Just before the participle. Yet belongs at the end in negatives."), + Q("English ___ all over the building.", ["speaks", "is spoken", "is spoke", "speak"], 1, "Trpný rod."), + Q("There isn't ___ time.", ["many", "much", "a few", "several"], 1, "Time je nepočitatelné: much."), + Q("He asked me where ___.", ["did I live", "I lived", "do I lived", "lived I"], 1, "Bez inverze."), + Q("You ___ shout. The baby is asleep. It is forbidden.", ["don't have to", "mustn't", "might", "used to"], 1, "Zákaz: mustn't."), + ], + ), + L( + "Cvičná maturita", + "A practice paper", + "Čtení, poslech, jazyková kompetence a krátká písemná práce.", + "Reading, listening, language in use and a short piece of writing.", + [ + S( + "1 / 2 • Celá zkouška", + "Tři části, jeden mozek", + "Tip: didaktický test míchá poslech, čtení a jazykovou kompetenci. Písemná práce je zvlášť. Tady je zmenšený model.", + [ + "Nejdřív přečtěte zadání psaní, ať víte, kolik času si necháte.", + "U čtení nehledejte stejná slova. Hledejte stejný význam.", + "U poslechu zápor bývá ta správná odpověď.", + "Když váháte mezi dvěma, vyhoďte tu, která přidává informaci v textu neslyšenou.", + ], + ), + S( + "2 / 2 • Písemná práce", + "Účel, čtenář, rozsah", + "Tip: e-mail kamarádovi a stížnost úřadu se nepíšou stejným jazykem. Tady jde o kamaráda.", + [ + "Řekněte proč píšete hned v první větě.", + "Dejte tam čas a místo, ať text není mlha.", + "Jedna věta s because a jedna s if textu pomůžou.", + "Na konci otázka, která zní jako opravdový zájem.", + ], + ), + ], + reading=( + "The town library now lends tools as well as books. Hammers, " + "sewing machines and a cake tin can be borrowed for a week. " + "The idea came from a librarian who was tired of people buying " + "things they would use once. She said the workshop had been " + "visited by more teenagers than the reading room. Critics argue " + "that a library should stay quiet. She replies that a library " + "should be useful. If the experiment works, other branches will " + "copy it. Users have to return each object clean, and they must " + "not lend it to anyone else." + ), + readq=[ + Q( + "What can people borrow besides books?", + ["Cars", "Tools", "Flats", "Tickets to Berlin"], + 1, + "Hammers, sewing machines, a cake tin.", + ), + Q( + "Why did the idea start?", + [ + "People were buying things they would use once", + "The reading room was too popular", + "Teenagers hated books", + "The tools were illegal", + ], + 0, + "buying things they would use once.", + ), + Q( + "Who has visited the workshop more, according to her?", + ["Critics", "Teenagers, more than the reading room", "Other branches", "Nobody"], + 1, + "more teenagers than the reading room.", + ), + Q( + "What must users not do?", + ["Return the object", "Clean it", "Lend it to someone else", "Visit the library"], + 2, + "must not lend it to anyone else.", + ), + ], + listening=( + "Welcome to the night-train podcast. The 22:40 to Berlin has been " + "delayed by forty minutes because a tree is on the line near Ústí. " + "Passengers who already have a seat can wait in carriage four, where " + "tea is being served. If you have not checked in, please do it on " + "your phone. We would be grateful if you kept the corridor clear. " + "The delay might be shorter. We will know more at ten." + ), + listenq=[ + Q("How long is the delay?", ["14 minutes", "40 minutes", "4 hours", "22 minutes"], 1, "forty minutes."), + Q("Why?", ["Snow", "A tree on the line", "A missing driver", "A festival"], 1, "a tree is on the line."), + Q("Where can passengers with a seat wait?", ["Carriage four", "The airport", "Ústí station only", "The corridor"], 0, "carriage four."), + Q("What should people without a check-in do?", ["Buy tea", "Check in on their phone", "Leave the train", "Call the mayor"], 1, "do it on your phone."), + ], + quiz=[ + Q("Tea ___ in carriage four.", ["serves", "is being served", "is serving", "has serve"], 1, "Průběhový trpný rod."), + Q("We would be grateful if you ___ the corridor clear.", ["keep", "kept", "keeping", "to keep"], 1, "Would be grateful if + minulý tvar."), + Q("She said the workshop ___ by more teenagers.", ["visited", "had been visited", "has visit", "is visiting"], 1, "Had been visited."), + Q("Users ___ return the tools clean.", ["have to", "must to", "don't must", "are have"], 0, "Have to."), + ], + writing=W( + "Write at least 45 words to a friend. Invite them to borrow something from a library of things. Use because and if.", + "Hi Adam, the library now lends tools, and I think you would like it because you always need a drill for one afternoon. If you come on Saturday, I will show you how it works. You have to bring your card and return everything clean. We could borrow a cake tin and bake something after. Let me know if you are free.", + "because|if", + 45, + ), + ), + ], + } + + +YEARS.append(y3()) +YEARS.append(y4()) + + +def emit_questions(name, items, lines): + lines.append(f"static const ChoiceQ {name}[] = {{") + for prompt, options, correct, _hint in items: + opts = ", ".join(c_str(o) for o in options) + lines.append(f" {{{c_str(prompt)}, {{{opts}}}, 4, {correct}}},") + lines.append("};") + lines.append(f"static const char *{name}_hints[] = {{") + for _p, _o, _c, hint in items: + lines.append(f" {c_str(hint)},") + lines.append("};") + lines.append("") + + +def emit_lesson(year, index, lesson, lines, inits): + p = f"en_y{year}_l{index}" + lines.append(f"static const NetSlide {p}_slides[] = {{") + for kicker, title, tip, bullets in lesson["slides"]: + lines.append(" {") + lines.append(f" {c_str(kicker)}, {c_str(title)}, {c_str(tip)},") + lines.append(" {") + for bullet in bullets: + lines.append(f" {c_str(bullet)},") + lines.append(" NULL,") + lines.append(" },") + lines.append(" },") + lines.append("};") + lines.append("") + + init = [ + f" .slides = {p}_slides,", + f" .n_slides = (int)(sizeof {p}_slides / sizeof {p}_slides[0]),", + ] + + if lesson.get("reading"): + lines.append(f"static const char *{p}_read = {c_str(lesson['reading'])};") + lines.append("") + init.append(f" .reading = {c_str(lesson['reading'])},") + if lesson.get("readq"): + emit_questions(f"{p}_readq", lesson["readq"], lines) + init.append(f" .readq = {p}_readq,") + init.append(f" .read_hints = {p}_readq_hints,") + init.append(f" .n_read = (int)(sizeof {p}_readq / sizeof {p}_readq[0]),") + if lesson.get("listening"): + lines.append(f"static const char *{p}_listen = {c_str(lesson['listening'])};") + lines.append("") + init.append(f" .listening = {c_str(lesson['listening'])},") + if lesson.get("listenq"): + emit_questions(f"{p}_listenq", lesson["listenq"], lines) + init.append(f" .listenq = {p}_listenq,") + init.append(f" .listen_hints = {p}_listenq_hints,") + init.append(f" .n_listen = (int)(sizeof {p}_listenq / sizeof {p}_listenq[0]),") + if lesson.get("gaps"): + lines.append(f"static const TypedQ {p}_gaps[] = {{") + for prompt, answers, meaning in lesson["gaps"]: + lines.append(f" {{{c_str(prompt)}, {c_str(answers)}, {c_str(meaning)}}},") + lines.append("};") + lines.append("") + init.append(f" .gaps = {p}_gaps,") + init.append(f" .n_gaps = (int)(sizeof {p}_gaps / sizeof {p}_gaps[0]),") + if lesson.get("writing"): + prompt, model, keys, min_words = lesson["writing"] + lines.append(f"static const char *{p}_write[] = {{") + lines.append(f" {c_str(prompt)},") + lines.append(f" {c_str(model)},") + lines.append(f" {c_str(keys)},") + lines.append(f" {c_str(str(min_words))},") + lines.append("};") + lines.append("") + init.append(f" .write_prompt = {c_str(prompt)},") + init.append(f" .write_model = {c_str(model)},") + init.append(f" .write_keys = {c_str(keys)},") + init.append(f" .write_min = {min_words},") + if lesson.get("quiz"): + emit_questions(f"{p}_quiz", lesson["quiz"], lines) + init.append(f" .quiz = {p}_quiz,") + init.append(f" .quiz_hints = {p}_quiz_hints,") + init.append(f" .n_quiz = (int)(sizeof {p}_quiz / sizeof {p}_quiz[0]),") + + kinds = [k for k in ("reading", "listening", "writing", "gaps", "quiz", "readq", "listenq") if lesson.get(k)] + if len(kinds) < 2 and "reading" not in kinds and "listening" not in kinds: + raise SystemExit(f"thin lesson y{year} l{index}: {kinds}") + inits.append(" {\n" + "\n".join(init) + "\n }") + + +def main() -> None: + if len(YEARS) != 4 or any(len(y["lessons"]) != 8 for y in YEARS): + raise SystemExit("expected 4 years × 8 lessons") + + body = [ + "/* Generated by scripts/gen-english.py. Do not edit by hand. */", + "", + ] + table = ["static const EnLesson en_lessons[EN_YEARS][EN_N] = {"] + i18n = [ + ' {"en_years_sub", "Čtyři ročníky podle osnov gymnázia, od A2 k maturitě.", "Four years of the gymnasium syllabus, from A2 to the exam."},', + ' {"en_year1", "1. ročník", "Year 1"},', + ' {"en_year2", "2. ročník", "Year 2"},', + ' {"en_year3", "3. ročník", "Year 3"},', + ' {"en_year4", "4. ročník", "Year 4"},', + ' {"en_sec_read", "Čtení", "Reading"},', + ' {"en_sec_listen", "Poslech", "Listening"},', + ' {"en_sec_write", "Psaní", "Writing"},', + ' {"en_sec_gap", "Doplňte", "Complete"},', + ' {"en_sec_quiz", "Kvíz", "Quiz"},', + ' {"en_sec_test", "Jazyková část", "Language"},', + ' {"en_play", "Přehrát", "Play"},', + ' {"en_listen_note", "Nejdřív si přečtěte otázky. Nahrávku přehraje hlas zařízení. Bez něj se věty ukážou postupně.", "Read the questions first. The device voice plays the recording. Without one, the sentences appear one by one."},', + ' {"en_listen_first", "Nejdřív poslech přehrajte.", "Play the recording first."},', + ' {"en_listen_fallback", "Hlas není dostupný, věty se ukazují postupně.", "No voice is available, so the sentences appear one by one."},', + ' {"en_write_short", "Text je krátký nebo v něm chybí požadovaná slova.", "The text is short or it misses the required words."},', + ' {"en_model", "Vzor", "Model"},', + ' {"en_write_ph", "Pište anglicky…", "Write in English…"},', + ] + for yi, year in enumerate(YEARS, start=1): + i18n.append( + f' {{"en_y{yi}_sub", {c_str(year["sub_cs"])}, {c_str(year["sub_en"])}}},' + ) + row = [] + for li, lesson in enumerate(year["lessons"], start=1): + i18n.append( + f' {{"en_y{yi}_u{li}", {c_str(lesson["title_cs"])}, {c_str(lesson["title_en"])}}},' + ) + i18n.append( + f' {{"en_y{yi}_u{li}_sub", {c_str(lesson["sub_cs"])}, {c_str(lesson["sub_en"])}}},' + ) + inits = [] + emit_lesson(yi, li, lesson, body, inits) + row.append(inits[0]) + table.append(" {\n" + ",\n".join(row) + "\n },") + table.append("};") + table.append("") + body.extend(table) + + (ROOT / "src" / "english_lessons.inc").write_text("\n".join(body) + "\n", encoding="utf-8") + (ROOT / "src" / "english_i18n.inc").write_text("\n".join(i18n) + "\n", encoding="utf-8") + skills = [] + for yi, year in enumerate(YEARS, start=1): + bag = set() + for lesson in year["lessons"]: + bag.update(k for k in ("reading", "listening", "writing", "gaps", "quiz") if lesson.get(k)) + skills.append(f"year {yi}: {', '.join(sorted(bag))}") + print("wrote", len(YEARS) * 8, "lessons") + print("\n".join(skills)) + + +if __name__ == "__main__": + main() diff --git a/src/english.c b/src/english.c new file mode 100644 index 0000000..25e0d79 --- /dev/null +++ b/src/english.c @@ -0,0 +1,1331 @@ +#include "graduately.h" + +#include + +#ifdef G_OS_UNIX +#include +#endif + +/* English across four gymnasium years (RVP G, foreign language, maturita + * skills). Lesson text lives in english_lessons.inc. */ + +#define EN_YEARS 4 +#define EN_N 8 + +typedef struct { + const NetSlide *slides; + int n_slides; + const char *reading; + const ChoiceQ *readq; + const char **read_hints; + int n_read; + const char *listening; + const ChoiceQ *listenq; + const char **listen_hints; + int n_listen; + const TypedQ *gaps; + int n_gaps; + const char *write_prompt; + const char *write_model; + const char *write_keys; + int write_min; + const ChoiceQ *quiz; + const char **quiz_hints; + int n_quiz; +} EnLesson; + +#include "english_lessons.inc" + +typedef struct { + const ChoiceQ *q; + GtkToggleButton *tb[4]; + GtkWidget *hint; +} EnPick; + +typedef struct { + int year; + int index; + const EnLesson *lesson; + GtkWidget *feedback; + GtkWidget *model; + GArray *picks; + GArray *entries; + GArray *gap_hints; + GtkWidget *essay; + GtkWidget *script; + GtkWidget *play_note; + gboolean played; + gboolean busy; + gboolean fallback; + GPid pid; + guint timer; + int tick; + gchar **sentences; +} EnEx; + +typedef struct { + int year; + GtkWidget *scroll; + GtkWidget *fixed; + GtkWidget *rail; + GtkWidget *nodes[EN_N]; + GtkWidget *labels[EN_N]; + double cx[EN_N]; + double cy[EN_N]; + int cols, rows, cw, ch; + guint idle; + double last_avail; + int last_cols, last_rows, last_cw, last_ch; + cairo_surface_t *cache; + int cache_w, cache_h; +} EngMap; + +static NetLesson en_rt[EN_YEARS][EN_N]; +static EngMap en_maps[EN_YEARS]; +static char en_map_page[EN_YEARS][16]; +static char en_unit_page[EN_YEARS][EN_N][16]; +static char en_ex_page[EN_YEARS][EN_N][16]; +static char en_title_key[EN_YEARS][EN_N][16]; +static char en_sub_key[EN_YEARS][EN_N][20]; +static gboolean en_ready; + +static void en_prepare(void) { + if (en_ready) + return; + en_ready = TRUE; + for (int y = 0; y < EN_YEARS; y++) { + g_snprintf(en_map_page[y], sizeof en_map_page[y], "en%dmap", y + 1); + for (int i = 0; i < EN_N; i++) { + g_snprintf(en_unit_page[y][i], sizeof en_unit_page[y][i], + "en%dunit%d", y + 1, i + 1); + g_snprintf(en_ex_page[y][i], sizeof en_ex_page[y][i], + "en%dex%d", y + 1, i + 1); + g_snprintf(en_title_key[y][i], sizeof en_title_key[y][i], + "en_y%d_u%d", y + 1, i + 1); + g_snprintf(en_sub_key[y][i], sizeof en_sub_key[y][i], + "en_y%d_u%d_sub", y + 1, i + 1); + en_rt[y][i].unit_page = en_unit_page[y][i]; + en_rt[y][i].ex_page = en_ex_page[y][i]; + en_rt[y][i].n_slides = (guint)en_lessons[y][i].n_slides; + } + } +} + +static void en_save_progress(void) { + GKeyFile *kf = g_key_file_new(); + gchar *data; + GError *err = NULL; + + for (int y = 0; y < EN_YEARS; y++) { + for (int i = 0; i < EN_N; i++) { + char key[8]; + + g_snprintf(key, sizeof key, "%d-%d", y + 1, i + 1); + g_key_file_set_boolean(kf, "done", key, en_rt[y][i].done); + } + } + data = g_key_file_to_data(kf, NULL, NULL); + if (!g_file_set_contents(PROGRESS_EN, data, -1, &err)) { + g_warning("english progress: %s", err->message); + g_error_free(err); + } + g_free(data); + g_key_file_free(kf); +} + +void en_load_progress(void) { + GKeyFile *kf = g_key_file_new(); + GError *err = NULL; + + en_prepare(); + if (!g_key_file_load_from_file(kf, PROGRESS_EN, G_KEY_FILE_NONE, &err)) { + if (err) + g_error_free(err); + g_key_file_free(kf); + return; + } + for (int y = 0; y < EN_YEARS; y++) { + for (int i = 0; i < EN_N; i++) { + char key[8]; + + g_snprintf(key, sizeof key, "%d-%d", y + 1, i + 1); + en_rt[y][i].done = g_key_file_get_boolean(kf, "done", key, NULL); + } + } + g_key_file_free(kf); +} + +static void en_refresh(void) { + for (int y = 0; y < EN_YEARS; y++) { + for (int i = 0; i < EN_N; i++) { + GtkWidget *node = en_maps[y].nodes[i]; + GtkWidget *icon = en_rt[y][i].done_icon; + + if (!node) + continue; + if (en_rt[y][i].done) { + gtk_widget_remove_css_class(node, "current"); + gtk_widget_add_css_class(node, "done"); + if (icon) + gtk_widget_set_visible(icon, TRUE); + } else { + gtk_widget_remove_css_class(node, "done"); + gtk_widget_add_css_class(node, "current"); + if (icon) + gtk_widget_set_visible(icon, FALSE); + } + } + } +} + +static void mark_en_done(int year, int index) { + if (year < 0 || year >= EN_YEARS || index < 0 || index >= EN_N) + return; + if (en_rt[year][index].done) + return; + en_rt[year][index].done = TRUE; + en_save_progress(); + en_refresh(); + refresh_stats_ui(); +} + +void progress_for_en(ProgressSum *out) { + out->total_ex += EN_YEARS * EN_N; + out->open_units += EN_YEARS * EN_N; + for (int y = 0; y < EN_YEARS; y++) { + for (int i = 0; i < EN_N; i++) { + if (en_rt[y][i].done) { + out->done_ex++; + out->done_units++; + } + } + } +} + +/* ------------------------------------------------------------------ */ +/* Path */ +/* ------------------------------------------------------------------ */ + +static void en_point(EngMap *m, double t, double *ox, double *oy) { + int k = (int)t; + double u = t - (double)k; + double u2, u3; + double p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y; + + if (k < 0) { k = 0; u = 0.0; } + if (k >= EN_N - 1) { k = EN_N - 2; u = 1.0; } + + p1x = m->cx[k]; p1y = m->cy[k]; + p2x = m->cx[k + 1]; p2y = m->cy[k + 1]; + if (k - 1 >= 0) { p0x = m->cx[k - 1]; p0y = m->cy[k - 1]; } + else { p0x = p1x - (p2x - p1x); p0y = p1y - (p2y - p1y); } + if (k + 2 < EN_N) { p3x = m->cx[k + 2]; p3y = m->cy[k + 2]; } + else { p3x = p2x + (p2x - p1x); p3y = p2y + (p2y - p1y); } + + u2 = u * u; + u3 = u2 * u; + *ox = 0.5 * (2.0 * p1x + (-p0x + p2x) * u + + (2.0 * p0x - 5.0 * p1x + 4.0 * p2x - p3x) * u2 + + (-p0x + 3.0 * p1x - 3.0 * p2x + p3x) * u3); + *oy = 0.5 * (2.0 * p1y + (-p0y + p2y) * u + + (2.0 * p0y - 5.0 * p1y + 4.0 * p2y - p3y) * u2 + + (-p0y + 3.0 * p1y - 3.0 * p2y + p3y) * u3); +} + +static void en_path(EngMap *m, cairo_t *cr, double t0, double t1) { + int n = (int)((t1 - t0) * 24.0); + double x, y; + + if (n < 1) + n = 1; + en_point(m, t0, &x, &y); + cairo_move_to(cr, x, y); + for (int s = 1; s <= n; s++) { + en_point(m, t0 + (t1 - t0) * (double)s / (double)n, &x, &y); + cairo_line_to(cr, x, y); + } +} + +static void en_geometry(EngMap *m, double avail) { + const double phase = 2.0 * G_PI * 1.7 / (double)(EN_N - 1); + int rows; + + for (rows = 1; rows <= EN_N; rows++) { + int cols = (EN_N + rows - 1) / rows; + if (2.0 * ROAD_MX + (cols - 1) * PATH_SPAC <= avail + 1.0) + break; + } + if (rows > EN_N) + rows = EN_N; + m->rows = rows; + m->cols = (EN_N + rows - 1) / rows; + if (m->cols < 1) + m->cols = 1; + m->cw = (int)(2.0 * ROAD_MX + (m->cols - 1) * PATH_SPAC); + m->ch = (int)(2.0 * ROAD_MY + (rows - 1) * ROAD_GAP); + + for (int r = 0; r < rows; r++) { + int base = r * m->cols; + int len = MIN(m->cols, EN_N - base); + int fwd = (r % 2) == 0; + + for (int c = 0; c < len; c++) { + int i = base + c; + int cc = fwd ? c : (m->cols - 1 - c); + + m->cx[i] = ROAD_MX + cc * PATH_SPAC; + m->cy[i] = ROAD_MY + r * ROAD_GAP + + ROAD_WAVE * sin((double)i * phase); + } + } +} + +static void en_apply_layout(EngMap *m) { + if (!m->fixed) + return; + if (m->cols == m->last_cols && m->rows == m->last_rows && + m->cw == m->last_cw && m->ch == m->last_ch) + return; + m->last_cols = m->cols; + m->last_rows = m->rows; + m->last_cw = m->cw; + m->last_ch = m->ch; + if (m->cache) { + cairo_surface_destroy(m->cache); + m->cache = NULL; + } + for (int i = 0; i < EN_N; i++) { + if (!m->nodes[i] || !m->labels[i]) + continue; + gtk_fixed_move(GTK_FIXED(m->fixed), m->nodes[i], + (int)(m->cx[i] - NODE_SIZE / 2.0), + (int)(m->cy[i] - NODE_SIZE / 2.0)); + gtk_fixed_move(GTK_FIXED(m->fixed), m->labels[i], + (int)(m->cx[i] - (PATH_SPAC - 20.0) / 2.0), + (int)(m->cy[i] + NODE_SIZE / 2.0 + 10.0)); + } + gtk_widget_set_size_request(m->fixed, m->cw, m->ch); + gtk_widget_set_size_request(m->rail, m->cw, m->ch); + gtk_fixed_move(GTK_FIXED(m->fixed), m->rail, 0, 0); + gtk_widget_queue_draw(m->rail); +} + +static void en_relayout(EngMap *m) { + GtkAdjustment *hadj; + double avail; + + if (!m->scroll) + return; + hadj = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(m->scroll)); + avail = gtk_adjustment_get_page_size(hadj); + if (avail < 1.0) + return; + if (fabs(avail - m->last_avail) < 1.0) + return; + m->last_avail = avail; + en_geometry(m, avail); + en_apply_layout(m); +} + +static gboolean en_relayout_idle(gpointer data) { + EngMap *m = data; + + m->idle = 0; + en_relayout(m); + return G_SOURCE_REMOVE; +} + +static void en_adjust_notify(GtkAdjustment *adj, GParamSpec *ps, gpointer data) { + EngMap *m = data; + + (void)adj; + (void)ps; + if (m->idle == 0) + m->idle = g_idle_add(en_relayout_idle, m); +} + +static void en_render_rail(EngMap *m, cairo_t *cr) { + const Rgb rail = color_from_hex(app_theme.rail); + const Rgb muted = color_from_hex(app_theme.subtext); + + cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR); + cairo_paint(cr); + cairo_set_operator(cr, CAIRO_OPERATOR_OVER); + cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND); + cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND); + cairo_new_path(cr); + en_path(m, cr, 0.0, (double)(EN_N - 1)); + cairo_set_line_width(cr, 12); + cairo_set_source_rgb(cr, rail.r, rail.g, rail.b); + cairo_stroke(cr); + cairo_set_source_rgba(cr, muted.r, muted.g, muted.b, 0.17); + cairo_set_dash(cr, (double[]){2.0, 40.0}, 2, 0.0); + cairo_new_path(cr); + en_path(m, cr, 0.0, (double)(EN_N - 1)); + cairo_stroke(cr); + cairo_set_dash(cr, NULL, 0, 0.0); +} + +static void en_draw_rail(GtkDrawingArea *area, cairo_t *cr, + int width, int height, gpointer data) { + EngMap *m = data; + + (void)area; + (void)width; + (void)height; + if (!m->cache || m->cache_w != m->cw || m->cache_h != m->ch) { + cairo_t *rcr; + + if (m->cache) + cairo_surface_destroy(m->cache); + m->cache_w = m->cw > 0 ? m->cw : 1; + m->cache_h = m->ch > 0 ? m->ch : 1; + m->cache = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, + m->cache_w, m->cache_h); + rcr = cairo_create(m->cache); + en_render_rail(m, rcr); + cairo_destroy(rcr); + } + cairo_set_source_surface(cr, m->cache, 0, 0); + cairo_paint(cr); +} + +void en_rail_theme_reset(void) { + for (int y = 0; y < EN_YEARS; y++) { + if (en_maps[y].cache) { + cairo_surface_destroy(en_maps[y].cache); + en_maps[y].cache = NULL; + } + if (en_maps[y].rail) + gtk_widget_queue_draw(en_maps[y].rail); + } +} + +static void en_open_unit(GtkButton *button, gpointer data) { + NetLesson *L = data; + + (void)button; + if (!L) + return; + L->idx = 0; + if (L->stack) { + char name[16]; + + g_snprintf(name, sizeof name, "slide%u", L->idx); + gtk_stack_set_visible_child_name(GTK_STACK(L->stack), name); + if (L->prev_btn) + gtk_widget_set_sensitive(L->prev_btn, FALSE); + if (L->next_btn) + gtk_button_set_label(GTK_BUTTON(L->next_btn), tr("net_slide_next")); + } + gtk_stack_set_visible_child_name(main_stack, L->unit_page); +} + +static void en_add_node(EngMap *m, GtkFixed *fixed, int index) { + GtkWidget *card; + GtkWidget *vbox; + GtkWidget *number; + GtkWidget *icon; + GtkWidget *name; + char *text; + + card = gtk_button_new(); + gtk_widget_add_css_class(card, "unit-node"); + gtk_widget_add_css_class(card, "current"); + gtk_widget_set_can_focus(card, FALSE); + gtk_widget_set_size_request(card, (int)NODE_SIZE, (int)NODE_SIZE); + + vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2); + gtk_widget_set_halign(vbox, GTK_ALIGN_CENTER); + gtk_widget_set_valign(vbox, GTK_ALIGN_CENTER); + gtk_button_set_child(GTK_BUTTON(card), vbox); + + text = g_strdup_printf("%d", index + 1); + number = gtk_label_new(text); + g_free(text); + gtk_widget_add_css_class(number, "unit-number"); + gtk_box_append(GTK_BOX(vbox), number); + + icon = icon_area_new(draw_check_icon, 0.1176, 0.1176, 0.1804, 18); + gtk_widget_set_visible(icon, FALSE); + gtk_box_append(GTK_BOX(vbox), icon); + en_rt[m->year][index].done_icon = icon; + g_signal_connect(card, "clicked", G_CALLBACK(en_open_unit), + &en_rt[m->year][index]); + + m->nodes[index] = card; + gtk_fixed_put(fixed, card, 0, 0); + + name = gtk_label_new(NULL); + gtk_widget_set_size_request(name, (int)(PATH_SPAC - 20.0), -1); + gtk_widget_set_halign(name, GTK_ALIGN_CENTER); + gtk_label_set_justify(GTK_LABEL(name), GTK_JUSTIFY_CENTER); + gtk_label_set_wrap(GTK_LABEL(name), TRUE); + gtk_widget_add_css_class(name, "unit-name"); + i18n_bind(name, en_title_key[m->year][index], 0); + m->labels[index] = name; + gtk_fixed_put(fixed, name, 0, 0); +} + +static GtkWidget *en_year_button(int year) { + static const char *keys[] = {"en_year1", "en_year2", "en_year3", "en_year4"}; + static const char *subs[] = {"en_y1_sub", "en_y2_sub", "en_y3_sub", "en_y4_sub"}; + GtkWidget *btn; + GtkWidget *row; + GtkWidget *num; + GtkWidget *texts; + GtkWidget *title; + GtkWidget *sub; + char *num_text; + + btn = gtk_button_new(); + gtk_widget_add_css_class(btn, "unit-node"); + gtk_widget_add_css_class(btn, "current"); + gtk_widget_set_can_focus(btn, FALSE); + gtk_widget_set_hexpand(btn, TRUE); + gtk_widget_set_size_request(btn, -1, 112); + + row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 14); + gtk_widget_set_margin_start(row, 18); + gtk_widget_set_margin_end(row, 18); + gtk_widget_set_margin_top(row, 8); + gtk_widget_set_margin_bottom(row, 8); + gtk_button_set_child(GTK_BUTTON(btn), row); + + num_text = g_strdup_printf("%d", year); + num = gtk_label_new(num_text); + g_free(num_text); + gtk_widget_add_css_class(num, "unit-number"); + gtk_widget_set_valign(num, GTK_ALIGN_CENTER); + gtk_box_append(GTK_BOX(row), num); + + texts = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2); + gtk_widget_set_hexpand(texts, TRUE); + gtk_box_append(GTK_BOX(row), texts); + + title = gtk_label_new(NULL); + i18n_bind(title, keys[year - 1], 0); + gtk_widget_set_halign(title, GTK_ALIGN_START); + gtk_widget_add_css_class(title, "unit-name"); + gtk_box_append(GTK_BOX(texts), title); + + sub = gtk_label_new(NULL); + i18n_bind(sub, subs[year - 1], 0); + gtk_widget_set_halign(sub, GTK_ALIGN_START); + gtk_label_set_wrap(GTK_LABEL(sub), TRUE); + gtk_label_set_xalign(GTK_LABEL(sub), 0); + gtk_widget_add_css_class(sub, "meaning"); + gtk_widget_set_visible(sub, TRUE); + gtk_box_append(GTK_BOX(texts), sub); + + g_object_set_data_full(G_OBJECT(btn), "target", + g_strdup(en_map_page[year - 1]), g_free); + g_signal_connect(btn, "clicked", G_CALLBACK(on_nav_clicked), NULL); + return btn; +} + +GtkWidget *build_enyears_page(void) { + GtkWidget *page; + GtkWidget *scroll; + GtkWidget *list; + + en_prepare(); + page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_set_hexpand(page, TRUE); + gtk_widget_set_vexpand(page, TRUE); + gtk_widget_set_margin_start(page, 32); + gtk_widget_set_margin_end(page, 32); + gtk_widget_set_margin_top(page, 24); + gtk_widget_set_margin_bottom(page, 24); + gtk_box_append(GTK_BOX(page), + top_bar("subjects", "English", "en_years_sub")); + + scroll = gtk_scrolled_window_new(); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), + GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + gtk_widget_set_vexpand(scroll, TRUE); + gtk_widget_set_margin_top(scroll, 18); + gtk_box_append(GTK_BOX(page), scroll); + + list = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12); + gtk_widget_set_halign(list, GTK_ALIGN_CENTER); + gtk_widget_set_size_request(list, 460, -1); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), list); + for (int y = 1; y <= EN_YEARS; y++) + gtk_box_append(GTK_BOX(list), en_year_button(y)); + return page; +} + +GtkWidget *build_enmap_page(int year) { + EngMap *m; + GtkWidget *page; + GtkWidget *scroll; + GtkWidget *wrap; + GtkWidget *fixed; + GtkWidget *rail; + GtkAdjustment *ha; + GtkAdjustment *va; + static const char *year_keys[] = { + "en_year1", "en_year2", "en_year3", "en_year4", + }; + static const char *sub_keys[] = { + "en_y1_sub", "en_y2_sub", "en_y3_sub", "en_y4_sub", + }; + + en_prepare(); + m = &en_maps[year]; + m->year = year; + en_geometry(m, 1000.0); + + page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_set_hexpand(page, TRUE); + gtk_widget_set_vexpand(page, TRUE); + gtk_widget_set_margin_start(page, 32); + gtk_widget_set_margin_end(page, 32); + gtk_widget_set_margin_top(page, 24); + gtk_widget_set_margin_bottom(page, 24); + gtk_box_append(GTK_BOX(page), + top_bar("enyears", year_keys[year], sub_keys[year])); + + scroll = gtk_scrolled_window_new(); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), + GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_widget_set_vexpand(scroll, TRUE); + gtk_widget_set_margin_top(scroll, 14); + gtk_box_append(GTK_BOX(page), scroll); + m->scroll = scroll; + + wrap = gtk_center_box_new(); + gtk_widget_set_vexpand(wrap, TRUE); + gtk_widget_set_hexpand(wrap, TRUE); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), wrap); + + fixed = gtk_fixed_new(); + gtk_widget_set_halign(fixed, GTK_ALIGN_CENTER); + gtk_widget_set_valign(fixed, GTK_ALIGN_CENTER); + gtk_widget_set_size_request(fixed, m->cw, m->ch); + gtk_center_box_set_center_widget(GTK_CENTER_BOX(wrap), fixed); + m->fixed = fixed; + + rail = gtk_drawing_area_new(); + gtk_widget_set_size_request(rail, m->cw, m->ch); + gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(rail), en_draw_rail, m, NULL); + gtk_fixed_put(GTK_FIXED(fixed), rail, 0, 0); + m->rail = rail; + + for (int i = 0; i < EN_N; i++) + en_add_node(m, GTK_FIXED(fixed), i); + + ha = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(scroll)); + va = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(scroll)); + g_signal_connect(ha, "notify::page-size", G_CALLBACK(en_adjust_notify), m); + g_signal_connect(va, "notify::page-size", G_CALLBACK(en_adjust_notify), m); + en_refresh(); + return page; +} + +/* ------------------------------------------------------------------ */ +/* Slides */ +/* ------------------------------------------------------------------ */ + +static void en_slide_apply(NetLesson *L) { + char name[16]; + + if (!L || !L->stack) + return; + g_snprintf(name, sizeof name, "slide%u", L->idx); + gtk_stack_set_visible_child_name(GTK_STACK(L->stack), name); + if (L->prev_btn) { + gtk_widget_set_sensitive(L->prev_btn, L->idx > 0); + gtk_button_set_label(GTK_BUTTON(L->prev_btn), tr("net_slide_prev")); + } + if (L->next_btn) + gtk_button_set_label(GTK_BUTTON(L->next_btn), + L->idx + 1 >= L->n_slides ? tr("net_slide_start") + : tr("net_slide_next")); +} + +static int en_note_w = -1; +static gboolean en_note_ready; + +static void en_rescale_notes(int w) { + int body, head, kick; + + if (w < 160) + w = 160; + body = w / 46; + if (body < 14) + body = 14; + if (body > 26) + body = 26; + head = body + 14; + if (head > 46) + head = 46; + kick = body - 3; + if (kick < 11) + kick = 11; + for (int y = 0; y < EN_YEARS; y++) + for (int i = 0; i < EN_N; i++) + net_rescale_lesson_notes(&en_rt[y][i], body, head, kick); +} + +static gboolean en_note_tick(GtkWidget *w, GdkFrameClock *clock, gpointer data) { + int width = gtk_widget_get_allocated_width(w); + + (void)clock; + (void)data; + if (width != en_note_w) { + en_note_w = width; + en_rescale_notes(width); + } + return G_SOURCE_CONTINUE; +} + +static void en_slide_prev(GtkButton *button, gpointer data) { + NetLesson *L = data; + + (void)button; + if (L && L->idx > 0) { + L->idx--; + en_slide_apply(L); + } +} + +static void en_slide_next(GtkButton *button, gpointer data) { + NetLesson *L = data; + + (void)button; + if (!L) + return; + if (L->idx + 1 < L->n_slides) { + L->idx++; + en_slide_apply(L); + } else { + gtk_stack_set_visible_child_name(main_stack, L->ex_page); + } +} + +void en_lessons_apply_lang(void) { + for (int y = 0; y < EN_YEARS; y++) + for (int i = 0; i < EN_N; i++) + en_slide_apply(&en_rt[y][i]); +} + +static GtkWidget *build_en_unit_page(int year, int index) { + NetLesson *L = &en_rt[year][index]; + const EnLesson *lesson = &en_lessons[year][index]; + GtkWidget *page; + GtkWidget *scroll; + GtkWidget *nav; + + net_lesson_notes_ensure(L); + net_notes_target = L; + L->n_slides = (guint)lesson->n_slides; + + page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_set_margin_start(page, 28); + gtk_widget_set_margin_end(page, 28); + gtk_widget_set_margin_top(page, 20); + gtk_widget_set_margin_bottom(page, 20); + gtk_box_append(GTK_BOX(page), + top_bar(en_map_page[year], en_title_key[year][index], + en_sub_key[year][index])); + + scroll = gtk_scrolled_window_new(); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), + GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + gtk_widget_set_vexpand(scroll, TRUE); + gtk_widget_set_margin_top(scroll, 10); + gtk_box_append(GTK_BOX(page), scroll); + + L->stack = gtk_stack_new(); + gtk_stack_set_transition_type(GTK_STACK(L->stack), + GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), L->stack); + + for (guint s = 0; s < L->n_slides; s++) { + GtkWidget *card = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8); + char name[16]; + + g_snprintf(name, sizeof name, "slide%u", s); + gtk_widget_set_margin_top(card, 8); + gtk_widget_set_margin_bottom(card, 8); + net_note_kicker(card, lesson->slides[s].kicker); + net_note_title(card, lesson->slides[s].title); + for (int i = 0; i < 8 && lesson->slides[s].line[i]; i++) + net_note_line(card, lesson->slides[s].line[i], FALSE); + if (lesson->slides[s].tip) + net_note_line(card, lesson->slides[s].tip, TRUE); + gtk_stack_add_named(GTK_STACK(L->stack), card, name); + } + + nav = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10); + gtk_box_set_homogeneous(GTK_BOX(nav), TRUE); + gtk_widget_set_margin_top(nav, 10); + gtk_box_append(GTK_BOX(page), nav); + L->prev_btn = gtk_button_new_with_label(tr("net_slide_prev")); + gtk_widget_add_css_class(L->prev_btn, "btn-primary"); + gtk_box_append(GTK_BOX(nav), L->prev_btn); + g_signal_connect(L->prev_btn, "clicked", G_CALLBACK(en_slide_prev), L); + L->next_btn = gtk_button_new_with_label(tr("net_slide_next")); + gtk_widget_add_css_class(L->next_btn, "btn-primary"); + gtk_box_append(GTK_BOX(nav), L->next_btn); + g_signal_connect(L->next_btn, "clicked", G_CALLBACK(en_slide_next), L); + if (!en_note_ready) { + gtk_widget_add_tick_callback(L->stack, en_note_tick, NULL, NULL); + en_note_ready = TRUE; + } + en_rescale_notes(0); + net_notes_target = NULL; + en_slide_apply(L); + return page; +} + +/* ------------------------------------------------------------------ */ +/* Exercise */ +/* ------------------------------------------------------------------ */ + +static GtkWidget *en_heading(GtkWidget *body, const char *key) { + GtkWidget *label = gtk_label_new(NULL); + + i18n_bind(label, key, 0); + gtk_widget_set_halign(label, GTK_ALIGN_START); + gtk_label_set_wrap(GTK_LABEL(label), TRUE); + gtk_widget_add_css_class(label, "quiz-heading"); + gtk_widget_set_margin_top(label, 8); + gtk_box_append(GTK_BOX(body), label); + return label; +} + +static GtkWidget *en_passage(GtkWidget *body, const char *text, gboolean hide) { + GtkWidget *label = gtk_label_new(text); + + gtk_label_set_wrap(GTK_LABEL(label), TRUE); + gtk_label_set_wrap_mode(GTK_LABEL(label), PANGO_WRAP_WORD_CHAR); + gtk_label_set_selectable(GTK_LABEL(label), TRUE); + gtk_label_set_xalign(GTK_LABEL(label), 0); + gtk_widget_set_halign(label, GTK_ALIGN_START); + gtk_widget_add_css_class(label, "quiz-intro"); + gtk_widget_set_visible(label, !hide); + gtk_box_append(GTK_BOX(body), label); + return label; +} + +static void en_add_mcq(EnEx *ctx, GtkWidget *body, const ChoiceQ *qs, + const char **hints, int n) { + for (int i = 0; i < n; i++) { + EnPick pick; + GtkWidget *card; + + memset(&pick, 0, sizeof pick); + pick.q = &qs[i]; + card = mcq_append_question(body, i + 1, &qs[i], pick.tb); + if (hints && hints[i]) + pick.hint = meaning_add(card, hints[i]); + g_array_append_val(ctx->picks, pick); + } +} + +static int en_word_count(const char *norm) { + int n = 0; + const char *p = norm ? norm : ""; + + while (*p) { + while (*p == ' ') + p++; + if (!*p) + break; + n++; + while (*p && *p != ' ') + p++; + } + return n; +} + +static gboolean en_has_word(const char *norm, const char *word) { + char *key = normalize_answer(word); + size_t n; + const char *p; + gboolean ok = FALSE; + + if (!key || !key[0]) { + g_free(key); + return TRUE; + } + n = strlen(key); + for (p = norm; p && (p = strstr(p, key)); p += n) { + gboolean left = p == norm || p[-1] == ' '; + gboolean right = p[n] == '\0' || p[n] == ' '; + + if (left && right) { + ok = TRUE; + break; + } + } + g_free(key); + return ok; +} + +static void en_split_script(EnEx *ctx) { + GPtrArray *parts = g_ptr_array_new(); + const char *text = ctx->lesson->listening; + const char *start = text; + const char *p = text; + + while (p && *p) { + if ((*p == '.' || *p == '?' || *p == '!') && p[1] == ' ') { + g_ptr_array_add(parts, g_strndup(start, (gsize)(p - start + 1))); + p += 2; + start = p; + continue; + } + p++; + } + if (start && *start) + g_ptr_array_add(parts, g_strdup(start)); + if (parts->len == 0) + g_ptr_array_add(parts, g_strdup(text ? text : "")); + g_ptr_array_add(parts, NULL); + ctx->sentences = (gchar **)g_ptr_array_free(parts, FALSE); + ctx->tick = 0; +} + +static gboolean en_tick(gpointer data) { + EnEx *ctx = data; + GString *shown; + int i; + + if (!ctx->sentences || !ctx->sentences[ctx->tick]) { + ctx->timer = 0; + ctx->busy = FALSE; + if (ctx->script && ctx->lesson->listening) + gtk_label_set_text(GTK_LABEL(ctx->script), ctx->lesson->listening); + return G_SOURCE_REMOVE; + } + shown = g_string_new(NULL); + for (i = 0; i <= ctx->tick && ctx->sentences[i]; i++) { + if (i) + g_string_append_c(shown, ' '); + g_string_append(shown, ctx->sentences[i]); + } + gtk_label_set_text(GTK_LABEL(ctx->script), shown->str); + gtk_widget_set_visible(ctx->script, TRUE); + g_string_free(shown, TRUE); + ctx->tick++; + if (!ctx->sentences[ctx->tick]) { + ctx->timer = 0; + ctx->busy = FALSE; + return G_SOURCE_REMOVE; + } + return G_SOURCE_CONTINUE; +} + +static void en_start_fallback(EnEx *ctx) { + if (ctx->fallback) + return; + ctx->fallback = TRUE; + ctx->played = TRUE; + ctx->busy = TRUE; + if (ctx->play_note) { + gtk_label_set_text(GTK_LABEL(ctx->play_note), tr("en_listen_fallback")); + gtk_widget_set_visible(ctx->play_note, TRUE); + } + if (!ctx->sentences) + en_split_script(ctx); + ctx->tick = 0; + if (ctx->timer) + g_source_remove(ctx->timer); + ctx->timer = g_timeout_add(2200, en_tick, ctx); + en_tick(ctx); +} + +static void en_voice_done(GPid pid, gint status, gpointer data) { + EnEx *ctx = data; + gboolean ok = FALSE; + + g_spawn_close_pid(pid); + if (ctx->pid == pid) + ctx->pid = 0; +#ifdef G_OS_UNIX + ok = WIFEXITED(status) && WEXITSTATUS(status) == 0; +#else + ok = status == 0; +#endif + if (!ok) { + ctx->busy = FALSE; + en_start_fallback(ctx); + return; + } + ctx->busy = FALSE; + if (ctx->script && ctx->lesson->listening) { + gtk_label_set_text(GTK_LABEL(ctx->script), ctx->lesson->listening); + gtk_widget_set_visible(ctx->script, TRUE); + } +} + +static gboolean en_spawn(char **argv, GPid *pid) { + GError *err = NULL; + + if (!argv[0] || !g_find_program_in_path(argv[0])) + return FALSE; + if (!g_spawn_async(NULL, argv, NULL, + G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD, + NULL, NULL, pid, &err)) { + g_clear_error(&err); + return FALSE; + } + return TRUE; +} + +static void en_play_clicked(GtkButton *button, gpointer data) { + EnEx *ctx = data; + char *text; + + (void)button; + if (!ctx->lesson->listening || ctx->busy) + return; + ctx->played = TRUE; + ctx->busy = TRUE; + text = (char *)ctx->lesson->listening; +#ifdef __APPLE__ + { + char *tries[][8] = { + {"say", "-v", "Samantha", "-r", "150", text, NULL}, + {"say", "-v", "Daniel", "-r", "150", text, NULL}, + {"say", "-r", "150", text, NULL}, + }; + for (int i = 0; i < 3; i++) { + if (en_spawn(tries[i], &ctx->pid)) { + g_child_watch_add(ctx->pid, en_voice_done, ctx); + return; + } + } + } +#else + { + char *a[] = {"espeak-ng", "-v", "en", "-s", "140", text, NULL}; + char *b[] = {"espeak", "-v", "en", "-s", "140", text, NULL}; + char *c[] = {"spd-say", "-l", "en", text, NULL}; + char **tries[] = {a, b, c}; + + for (int i = 0; i < 3; i++) { + if (en_spawn(tries[i], &ctx->pid)) { + g_child_watch_add(ctx->pid, en_voice_done, ctx); + return; + } + } + } +#endif + ctx->busy = FALSE; + en_start_fallback(ctx); +} + +static void en_check(GtkButton *button, gpointer data) { + EnEx *ctx = data; + int mcq_ok = 0; + int gap_ok = 0; + gboolean write_ok = TRUE; + gboolean all; + + (void)button; + if (ctx->lesson->listening && !ctx->played) { + set_feedback(ctx->feedback, FALSE, tr("en_listen_first")); + return; + } + + for (guint i = 0; i < ctx->picks->len; i++) { + EnPick *p = &g_array_index(ctx->picks, EnPick, i); + int active = -1; + + for (int o = 0; o < p->q->n_options; o++) { + gtk_widget_remove_css_class(GTK_WIDGET(p->tb[o]), "ok"); + gtk_widget_remove_css_class(GTK_WIDGET(p->tb[o]), "wrong"); + if (gtk_toggle_button_get_active(p->tb[o])) + active = o; + } + if (active == p->q->correct) { + mcq_ok++; + if (active >= 0) + gtk_widget_add_css_class(GTK_WIDGET(p->tb[active]), "ok"); + } else if (active >= 0) { + gtk_widget_add_css_class(GTK_WIDGET(p->tb[active]), "wrong"); + } + if (p->hint) + gtk_widget_set_visible(p->hint, TRUE); + } + + for (guint i = 0; i < ctx->entries->len; i++) { + GtkWidget *entry = g_array_index(ctx->entries, GtkWidget *, i); + const char *txt = gtk_editable_get_text(GTK_EDITABLE(entry)); + char *norm = normalize_answer(txt); + gboolean ok = answer_accepts(norm, ctx->lesson->gaps[i].answers); + + gtk_widget_remove_css_class(entry, "answer-ok"); + gtk_widget_remove_css_class(entry, "answer-wrong"); + gtk_widget_add_css_class(entry, ok ? "answer-ok" : "answer-wrong"); + if (ok) + gap_ok++; + g_free(norm); + if (i < ctx->gap_hints->len) { + GtkWidget *hint = g_array_index(ctx->gap_hints, GtkWidget *, i); + + if (hint) + gtk_widget_set_visible(hint, TRUE); + } + } + + if (ctx->essay && ctx->lesson->write_prompt) { + GtkTextBuffer *buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(ctx->essay)); + GtkTextIter a, b; + char *raw; + char *norm; + gboolean keys = TRUE; + + gtk_text_buffer_get_bounds(buf, &a, &b); + raw = gtk_text_buffer_get_text(buf, &a, &b, FALSE); + norm = normalize_answer(raw); + if (ctx->lesson->write_keys) { + gchar **parts = g_strsplit(ctx->lesson->write_keys, "|", -1); + + for (int i = 0; parts[i]; i++) { + if (parts[i][0] && !en_has_word(norm, parts[i])) + keys = FALSE; + } + g_strfreev(parts); + } + write_ok = keys && en_word_count(norm) >= ctx->lesson->write_min; + g_free(norm); + g_free(raw); + } + + all = mcq_ok == (int)ctx->picks->len + && gap_ok == ctx->lesson->n_gaps + && write_ok; + if (all) { + set_feedback(ctx->feedback, TRUE, tr("feedback_ok")); + if (ctx->model) + gtk_widget_set_visible(ctx->model, TRUE); + mark_en_done(ctx->year, ctx->index); + } else if (ctx->essay && !write_ok && + mcq_ok == (int)ctx->picks->len && + gap_ok == ctx->lesson->n_gaps) { + set_feedback(ctx->feedback, FALSE, tr("en_write_short")); + } else { + set_feedback(ctx->feedback, FALSE, tr("feedback_retry_short")); + } +} + +static GtkWidget *build_en_ex_page(int year, int index) { + const EnLesson *lesson = &en_lessons[year][index]; + EnEx *ctx = g_new0(EnEx, 1); + GtkWidget *page; + GtkWidget *scroll; + GtkWidget *body; + GtkWidget *check; + + ctx->year = year; + ctx->index = index; + ctx->lesson = lesson; + ctx->picks = g_array_new(FALSE, FALSE, sizeof(EnPick)); + ctx->entries = g_array_new(FALSE, FALSE, sizeof(GtkWidget *)); + ctx->gap_hints = g_array_new(FALSE, TRUE, sizeof(GtkWidget *)); + + page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_set_margin_start(page, 32); + gtk_widget_set_margin_end(page, 32); + gtk_widget_set_margin_top(page, 24); + gtk_widget_set_margin_bottom(page, 24); + gtk_box_append(GTK_BOX(page), + top_bar(en_unit_page[year][index], + en_title_key[year][index], NULL)); + + scroll = gtk_scrolled_window_new(); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), + GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_widget_set_vexpand(scroll, TRUE); + gtk_widget_set_margin_top(scroll, 12); + gtk_box_append(GTK_BOX(page), scroll); + + body = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12); + gtk_widget_set_hexpand(body, TRUE); + gtk_widget_set_margin_bottom(body, 12); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), body); + + if (lesson->reading) { + en_heading(body, lesson->n_read ? "en_sec_read" : "en_sec_read"); + en_passage(body, lesson->reading, FALSE); + if (lesson->n_read) + en_add_mcq(ctx, body, lesson->readq, lesson->read_hints, lesson->n_read); + } + + if (lesson->listening) { + GtkWidget *play; + GtkWidget *note; + + en_heading(body, "en_sec_listen"); + note = gtk_label_new(NULL); + i18n_bind(note, "en_listen_note", 0); + gtk_label_set_wrap(GTK_LABEL(note), TRUE); + gtk_label_set_xalign(GTK_LABEL(note), 0); + gtk_widget_set_halign(note, GTK_ALIGN_START); + gtk_widget_add_css_class(note, "quiz-intro"); + gtk_box_append(GTK_BOX(body), note); + + play = gtk_button_new(); + i18n_bind(play, "en_play", 1); + gtk_widget_add_css_class(play, "btn-primary"); + gtk_widget_set_halign(play, GTK_ALIGN_START); + gtk_box_append(GTK_BOX(body), play); + g_signal_connect(play, "clicked", G_CALLBACK(en_play_clicked), ctx); + + ctx->play_note = gtk_label_new(""); + gtk_label_set_wrap(GTK_LABEL(ctx->play_note), TRUE); + gtk_widget_set_halign(ctx->play_note, GTK_ALIGN_START); + gtk_widget_add_css_class(ctx->play_note, "meaning"); + gtk_widget_set_visible(ctx->play_note, FALSE); + gtk_box_append(GTK_BOX(body), ctx->play_note); + + ctx->script = en_passage(body, "", TRUE); + if (lesson->n_listen) + en_add_mcq(ctx, body, lesson->listenq, lesson->listen_hints, + lesson->n_listen); + } + + if (lesson->n_gaps) { + en_heading(body, "en_sec_gap"); + for (int i = 0; i < lesson->n_gaps; i++) { + GtkWidget *card = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6); + GtkWidget *prompt = gtk_label_new(NULL); + GtkWidget *entry = gtk_entry_new(); + GtkWidget *hint = NULL; + char *text = g_strdup_printf("%d.) %s", i + 1, lesson->gaps[i].prompt); + + gtk_widget_add_css_class(card, "quiz-card"); + gtk_label_set_text(GTK_LABEL(prompt), text); + g_free(text); + gtk_label_set_wrap(GTK_LABEL(prompt), TRUE); + gtk_label_set_xalign(GTK_LABEL(prompt), 0); + gtk_widget_set_halign(prompt, GTK_ALIGN_START); + gtk_box_append(GTK_BOX(card), prompt); + gtk_editable_set_width_chars(GTK_EDITABLE(entry), 24); + gtk_widget_set_halign(entry, GTK_ALIGN_START); + gtk_box_append(GTK_BOX(card), entry); + if (lesson->gaps[i].meaning) + hint = meaning_add(card, lesson->gaps[i].meaning); + gtk_box_append(GTK_BOX(body), card); + g_array_append_val(ctx->entries, entry); + g_array_append_val(ctx->gap_hints, hint); + } + } + + if (lesson->write_prompt) { + GtkWidget *prompt; + GtkWidget *view; + + en_heading(body, "en_sec_write"); + prompt = gtk_label_new(lesson->write_prompt); + gtk_label_set_wrap(GTK_LABEL(prompt), TRUE); + gtk_label_set_xalign(GTK_LABEL(prompt), 0); + gtk_widget_set_halign(prompt, GTK_ALIGN_START); + gtk_widget_add_css_class(prompt, "quiz-q"); + gtk_box_append(GTK_BOX(body), prompt); + + view = gtk_text_view_new(); + gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(view), GTK_WRAP_WORD_CHAR); + gtk_widget_set_size_request(view, -1, 140); + gtk_widget_add_css_class(view, "quiz-card"); + gtk_box_append(GTK_BOX(body), view); + ctx->essay = view; + + ctx->model = gtk_label_new(lesson->write_model); + gtk_label_set_wrap(GTK_LABEL(ctx->model), TRUE); + gtk_label_set_selectable(GTK_LABEL(ctx->model), TRUE); + gtk_label_set_xalign(GTK_LABEL(ctx->model), 0); + gtk_widget_set_halign(ctx->model, GTK_ALIGN_START); + gtk_widget_add_css_class(ctx->model, "meaning"); + gtk_widget_set_visible(ctx->model, FALSE); + gtk_box_append(GTK_BOX(body), ctx->model); + } + + if (lesson->n_quiz) { + en_heading(body, "en_sec_quiz"); + en_add_mcq(ctx, body, lesson->quiz, lesson->quiz_hints, lesson->n_quiz); + } + + check = gtk_button_new(); + i18n_bind(check, "check", 1); + gtk_widget_add_css_class(check, "btn-primary"); + gtk_widget_set_halign(check, GTK_ALIGN_START); + gtk_widget_set_margin_top(check, 8); + gtk_box_append(GTK_BOX(body), check); + g_signal_connect(check, "clicked", G_CALLBACK(en_check), ctx); + + ctx->feedback = gtk_label_new(""); + gtk_widget_set_halign(ctx->feedback, GTK_ALIGN_START); + gtk_label_set_wrap(GTK_LABEL(ctx->feedback), TRUE); + gtk_box_append(GTK_BOX(body), ctx->feedback); + return page; +} + +void add_en_pages(GtkStack *stack) { + en_prepare(); + for (int y = 0; y < EN_YEARS; y++) { + gtk_stack_add_named(stack, build_enmap_page(y), en_map_page[y]); + for (int i = 0; i < EN_N; i++) { + gtk_stack_add_named(stack, build_en_unit_page(y, i), + en_unit_page[y][i]); + gtk_stack_add_named(stack, build_en_ex_page(y, i), + en_ex_page[y][i]); + } + } +} + +void draw_uk_flag(GtkDrawingArea *area, cairo_t *cr, + int width, int height, gpointer data) { + double cx = width / 2.0; + double cy = height / 2.0; + double r = MIN(width, height) / 2.0 - 2.0; + double w = (double)width; + double h = (double)height; + + (void)area; + (void)data; + cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR); + cairo_paint(cr); + cairo_set_operator(cr, CAIRO_OPERATOR_OVER); + cairo_save(cr); + cairo_arc(cr, cx, cy, r, 0.0, 2.0 * G_PI); + cairo_clip(cr); + + cairo_set_source_rgb(cr, 0.01, 0.14, 0.40); + cairo_paint(cr); + + cairo_set_line_cap(cr, CAIRO_LINE_CAP_SQUARE); + cairo_set_source_rgb(cr, 1.0, 1.0, 1.0); + cairo_set_line_width(cr, w * 0.16); + cairo_move_to(cr, 0, 0); + cairo_line_to(cr, w, h); + cairo_move_to(cr, w, 0); + cairo_line_to(cr, 0, h); + cairo_stroke(cr); + + cairo_set_source_rgb(cr, 0.80, 0.05, 0.15); + cairo_set_line_width(cr, w * 0.055); + cairo_move_to(cr, 0, 0); + cairo_line_to(cr, w, h); + cairo_move_to(cr, w, 0); + cairo_line_to(cr, 0, h); + cairo_stroke(cr); + + cairo_set_source_rgb(cr, 1.0, 1.0, 1.0); + cairo_rectangle(cr, 0, h * 0.38, w, h * 0.24); + cairo_rectangle(cr, w * 0.38, 0, w * 0.24, h); + cairo_fill(cr); + + cairo_set_source_rgb(cr, 0.80, 0.05, 0.15); + cairo_rectangle(cr, 0, h * 0.43, w, h * 0.14); + cairo_rectangle(cr, w * 0.43, 0, w * 0.14, h); + cairo_fill(cr); + + cairo_restore(cr); +} diff --git a/src/english_i18n.inc b/src/english_i18n.inc new file mode 100644 index 0000000..e489961 --- /dev/null +++ b/src/english_i18n.inc @@ -0,0 +1,86 @@ + {"en_years_sub", "Čtyři ročníky podle osnov gymnázia, od A2 k maturitě.", "Four years of the gymnasium syllabus, from A2 to the exam."}, + {"en_year1", "1. ročník", "Year 1"}, + {"en_year2", "2. ročník", "Year 2"}, + {"en_year3", "3. ročník", "Year 3"}, + {"en_year4", "4. ročník", "Year 4"}, + {"en_sec_read", "Čtení", "Reading"}, + {"en_sec_listen", "Poslech", "Listening"}, + {"en_sec_write", "Psaní", "Writing"}, + {"en_sec_gap", "Doplňte", "Complete"}, + {"en_sec_quiz", "Kvíz", "Quiz"}, + {"en_sec_test", "Jazyková část", "Language"}, + {"en_play", "Přehrát", "Play"}, + {"en_listen_note", "Nejdřív si přečtěte otázky. Nahrávku přehraje hlas zařízení. Bez něj se věty ukážou postupně.", "Read the questions first. The device voice plays the recording. Without one, the sentences appear one by one."}, + {"en_listen_first", "Nejdřív poslech přehrajte.", "Play the recording first."}, + {"en_listen_fallback", "Hlas není dostupný, věty se ukazují postupně.", "No voice is available, so the sentences appear one by one."}, + {"en_write_short", "Text je krátký nebo v něm chybí požadovaná slova.", "The text is short or it misses the required words."}, + {"en_model", "Vzor", "Model"}, + {"en_write_ph", "Pište anglicky…", "Write in English…"}, + {"en_y1_sub", "Osobní život, škola, domov a minulost. Úroveň A2.", "Personal life, school, home and the past. Level A2."}, + {"en_y1_u1", "Já a moje rodina", "Me and my family"}, + {"en_y1_u1_sub", "Sloveso be, have got a přivlastňovací zájmena. Doplňování a kvíz.", "The verb be, have got and possessives. Gaps and a quiz."}, + {"en_y1_u2", "Školní den", "A school day"}, + {"en_y1_u2_sub", "Přítomný čas prostý. Čtení a doplňování.", "The present simple. Reading and gaps."}, + {"en_y1_u3", "Víkend ve městě", "A weekend in town"}, + {"en_y1_u3_sub", "Přítomný čas průběhový pro domluvené plány. Poslech.", "The present continuous for arrangements. Listening."}, + {"en_y1_u4", "Můj pokoj", "My room"}, + {"en_y1_u4_sub", "There is / there are a předložky místa. Krátký popis a kvíz.", "There is / there are and prepositions. A short description and a quiz."}, + {"en_y1_u5", "Na trhu", "At the market"}, + {"en_y1_u5_sub", "Počitatelnost, some a any, how much a how many. Čtení a doplňování.", "Countability, some and any, how much and how many. Reading and gaps."}, + {"en_y1_u6", "Minulé léto", "Last summer"}, + {"en_y1_u6_sub", "Minulý čas prostý, pravidelná i nepravidelná slovesa. Poslech a kvíz.", "The past simple, regular and irregular verbs. Listening and a quiz."}, + {"en_y1_u7", "Zpráva kamarádovi", "A message to a friend"}, + {"en_y1_u7_sub", "Will a going to. Krátká neformální zpráva a doplňování.", "Will and going to. A short informal message and gaps."}, + {"en_y1_u8", "Test 1. ročníku", "Year 1 test"}, + {"en_y1_u8_sub", "Čtení, poslech, gramatika a krátké psaní. Úroveň A2.", "Reading, listening, grammar and a short piece of writing. Level A2."}, + {"en_y2_sub", "Cestování, zdraví, práce, média a příroda. Úroveň B1.", "Travel, health, work, the media and nature. Level B1."}, + {"en_y2_u1", "Na cestách", "On the road"}, + {"en_y2_u1_sub", "Stupňování a minulý čas průběhový. Doplňování a kvíz.", "Comparison and the past continuous. Gaps and a quiz."}, + {"en_y2_u2", "U lékaře", "At the clinic"}, + {"en_y2_u2_sub", "Should, must a have to. Čtení letáku a kvíz.", "Should, must and have to. A leaflet and a quiz."}, + {"en_y2_u3", "První brigáda", "A first job"}, + {"en_y2_u3_sub", "Předpřítomný čas, for a since. Poslech pohovoru a doplňování.", "The present perfect, for and since. A job interview and gaps."}, + {"en_y2_u4", "Recenze filmu", "A film review"}, + {"en_y2_u4_sub", "Vztažné věty who, which a that. Krátká recenze a kvíz.", "Relative clauses with who, which and that. A short review and a quiz."}, + {"en_y2_u5", "Počasí a výlet", "Weather and a trip"}, + {"en_y2_u5_sub", "Too a enough, will a going to v předpovědi. Čtení a doplňování.", "Too and enough, will and going to in a forecast. Reading and gaps."}, + {"en_y2_u6", "Festival", "A festival"}, + {"en_y2_u6_sub", "Předpřítomný čas proti minulému. Poslech a kvíz.", "Present perfect versus past simple. Listening and a quiz."}, + {"en_y2_u7", "Kdyby", "If I could"}, + {"en_y2_u7_sub", "První a druhá podmínková věta. Psaní a doplňování.", "First and second conditionals. Writing and gaps."}, + {"en_y2_u8", "Test 2. ročníku", "Year 2 test"}, + {"en_y2_u8_sub", "Čtení, poslech, gramatika B1 a krátké psaní.", "Reading, listening, B1 grammar and a short piece of writing."}, + {"en_y3_sub", "Společnost, prostředí, média a složitější gramatika. Úroveň B1 až B2.", "Society, the environment, the media and harder grammar. Level B1 to B2."}, + {"en_y3_u1", "Jak se to vyrábí", "How things are made"}, + {"en_y3_u1_sub", "Trpný rod v přítomnosti a minulosti. Doplňování a kvíz.", "The present and past passive. Gaps and a quiz."}, + {"en_y3_u2", "Škola, o které se mluví", "A school people talk about"}, + {"en_y3_u2_sub", "Nepřímá řeč oznamovací. Čtení a kvíz.", "Reported statements. Reading and a quiz."}, + {"en_y3_u3", "Poslední buk", "The last beech"}, + {"en_y3_u3_sub", "Podmínky a slovník prostředí. Poslech podcastu a doplňování.", "Conditionals and environment vocabulary. A podcast and gaps."}, + {"en_y3_u4", "Pro a proti", "For and against"}, + {"en_y3_u4_sub", "Stavba úvahy a spojovací výrazy. Esej a kvíz.", "Essay structure and linking words. An essay and a quiz."}, + {"en_y3_u5", "Kdo to musel být", "It must have been"}, + {"en_y3_u5_sub", "Modální slovesa dedukce. Čtení záhady a kvíz.", "Modals of deduction. A short mystery and a quiz."}, + {"en_y3_u6", "Zprávy dne", "The news desk"}, + {"en_y3_u6_sub", "Nepřímé otázky. Poslech rozhovoru a kvíz.", "Reported questions. An interview and a quiz."}, + {"en_y3_u7", "Frázová slovesa v příběhu", "Phrasal verbs in a story"}, + {"en_y3_u7_sub", "Běžná frázová slovesa. Doplňování a kvíz.", "Common phrasal verbs. Gaps and a quiz."}, + {"en_y3_u8", "Test 3. ročníku", "Year 3 test"}, + {"en_y3_u8_sub", "Čtení, poslech, gramatika B1+ a krátká úvaha.", "Reading, listening, B1+ grammar and a short argument."}, + {"en_y4_sub", "Maturitní dovednosti: mluvení, formální psaní, čtení, poslech a gramatika. Úroveň B2.", "Exam skills: speaking, formal writing, reading, listening and grammar. Level B2."}, + {"en_y4_u1", "Maturitní kartička", "The speaking card"}, + {"en_y4_u1_sub", "Strategie ústní zkoušky a kolokace osobních témat. Kvíz a doplňování.", "Speaking-exam strategy and personal-topic collocations. A quiz and gaps."}, + {"en_y4_u2", "Formální e-mail", "A formal email"}, + {"en_y4_u2_sub", "Žádost a stížnost. Psaní a kvíz rejstříku.", "A request and a complaint. Writing and a register quiz."}, + {"en_y4_u3", "Noční vlak", "The night train"}, + {"en_y4_u3_sub", "Delší čtení ve stylu didaktického testu. Čtyři otázky k článku.", "A longer reading in the style of the written paper. Four questions."}, + {"en_y4_u4", "Tábor ve Walesu", "A camp in Wales"}, + {"en_y4_u4_sub", "Delší poslech: pohovor. Pět otázek.", "A longer listening: an interview. Five questions."}, + {"en_y4_u5", "Názorová esej", "An opinion essay"}, + {"en_y4_u5_sub", "Struktura názoru. Esej a kvíz o odstavcích.", "Opinion-essay structure. An essay and a quiz about paragraphs."}, + {"en_y4_u6", "Slovní zásoba okruhů", "Topic vocabulary"}, + {"en_y4_u6_sub", "Kolokace k maturitním tématům. Doplňování a kvíz.", "Collocations for the exam topics. Gaps and a quiz."}, + {"en_y4_u7", "Gramatická ambulance", "Grammar clinic"}, + {"en_y4_u7_sub", "Smíšené jevy k didaktickému testu. Kvíz a doplňování.", "Mixed points for the language paper. A quiz and gaps."}, + {"en_y4_u8", "Cvičná maturita", "A practice paper"}, + {"en_y4_u8_sub", "Čtení, poslech, jazyková kompetence a krátká písemná práce.", "Reading, listening, language in use and a short piece of writing."}, diff --git a/src/english_lessons.inc b/src/english_lessons.inc new file mode 100644 index 0000000..e6d502c --- /dev/null +++ b/src/english_lessons.inc @@ -0,0 +1,1907 @@ +/* Generated by scripts/gen-english.py. Do not edit by hand. */ + +static const NetSlide en_y1_l1_slides[] = { + { + "1 / 2 • Be", "Kdo jsem", "Tip: věk se říká slovesem be. „She has 16 years“ je čechismus.", + { + "I am, you are, he/she/it is, we/you/they are.", + "Zápor a otázka: She isn't tall. Are you from Prague?", + "Věk, povolání i nálada: I am 16. My mum is a nurse. I am fine.", + "Přivlastňovací zájmena: my, your, his, her, its, our, their.", + NULL, + }, + }, + { + "2 / 2 • Have got", "Co mám", "Tip: have got je britská hovorová vazba pro vlastnictví, ne pro věk.", + { + "I've got a younger brother. She hasn't got a pet.", + "Have you got any cousins? — Yes, I have. / No, I haven't.", + "V americké angličtině stačí have: I have a brother.", + "Rodina: parents, siblings, only child, get on well, look like.", + NULL, + }, + }, +}; + +static const TypedQ en_y1_l1_gaps[] = { + {"My sister ___ sixteen.", "is", "Věk: be, ne have."}, + {"___ you got a bike?", "Have", "Otázka s have got."}, + {"This is ___ grandmother. She lives with us.", "our", "Přivlastňovací zájmeno."}, + {"They ___ not from Brno. They are from Olomouc.", "are", "They + are."}, + {"He ___ got a dog, but he has got two cats.", "hasn't|has not", "Zápor have got."}, +}; + +static const ChoiceQ en_y1_l1_quiz[] = { + {"Which sentence is correct?", {"She has 16 years.", "She is 16 years old.", "She is 16 years.", "She has 16 years old."}, 4, 1}, + {"I ___ two cousins and one aunt.", {"am", "have got", "has got", "is"}, 4, 1}, + {"Eva is my sister. ___ room is next to mine.", {"She", "Her", "His", "Hers"}, 4, 1}, + {"___ your parents at home?", {"Is", "Are", "Have", "Do"}, 4, 1}, +}; +static const char *en_y1_l1_quiz_hints[] = { + "Věk: be + years old.", + "Vlastnictví v 1. osobě: have got.", + "Před podstatným jménem je her, ne she.", + "Parents je množné číslo: Are.", +}; + +static const NetSlide en_y1_l2_slides[] = { + { + "1 / 2 • Present simple", "Co dělám pravidelně", "Tip: ve 3. osobě jednotného čísla přibývá -s/-es: she watches.", + { + "I get up at seven. He gets up at six.", + "Zápor: I don't like maths. She doesn't eat meat.", + "Otázka: Do you walk to school? Does she play the piano?", + "Příslovce četnosti stojí před slovesem: I usually walk. She is always late.", + NULL, + }, + }, + { + "2 / 2 • Škola", "Rozvrh a přestávky", "Tip: předmět ve škole je bez členu, když mluvíme obecně: I like biology.", + { + "lessons, break, canteen, homework, timetable, subject.", + "Čas: at eight, at half past twelve, in the morning, on Monday.", + "because vysvětluje důvod: I like biology because we do experiments.", + "never / sometimes / often / usually / always.", + NULL, + }, + }, +}; + +static const char *en_y1_l2_read = "My school day starts early. I get up at half past six, have breakfast and catch the tram. Lessons begin at eight. On Monday we have maths, English and biology. I like biology because we do experiments, but I don't like maths. We have lunch at half past twelve in the canteen. After school I do my homework or meet friends. I never go to bed after ten on a school night."; + +static const ChoiceQ en_y1_l2_readq[] = { + {"What time do lessons begin?", {"At half past six", "At eight", "At half past twelve", "At ten"}, 4, 1}, + {"Why does the writer like biology?", {"Because it is easy", "Because they do experiments", "Because the teacher is funny", "Because there is no homework"}, 4, 1}, + {"Where do they have lunch?", {"At home", "On the tram", "In the canteen", "In the park"}, 4, 2}, + {"On a school night the writer goes to bed…", {"after eleven", "at midnight", "never after ten", "before six"}, 4, 2}, +}; +static const char *en_y1_l2_readq_hints[] = { + "Lessons begin at eight.", + "because we do experiments.", + "in the canteen.", + "never go to bed after ten.", +}; + +static const TypedQ en_y1_l2_gaps[] = { + {"She ___ (watch) a series every evening.", "watches", "3. osoba: watches."}, + {"We ___ (not go) to school on Sunday.", "don't go|do not go", "Zápor v 1. osobě množného čísla."}, + {"___ he play football on Fridays?", "Does", "Otázka ve 3. osobě."}, + {"I usually ___ (get) up at seven.", "get", "Usually nemění tvar slovesa."}, +}; + +static const NetSlide en_y1_l3_slides[] = { + { + "1 / 2 • Continuous", "Co právě probíhá a co je domluvené", "Tip: I'm meeting Eva at ten je plán, ne jen „potkávám“.", + { + "Tvar: am/is/are + -ing. I'm waiting. She isn't coming.", + "Teď: Look, it is raining. Please be quiet, the baby is sleeping.", + "Blízký plán: We're having lunch on Saturday. I'm playing volleyball.", + "Stavová slovesa obvykle průběhový čas nemají: I know, I like, I want.", + NULL, + }, + }, + { + "2 / 2 • Poslech", "Jak poslouchat vzkaz", "Tip: nejdřív si přečtěte otázky, pak teprve pusťte nahrávku.", + { + "Chyťte jména, časy a místa. Ta se v otázkách opakují.", + "Nenechte se zastavit jedním slovem. Poslouchejte dál.", + "I'm meeting, we're going, I'm playing jsou v tomhle vzkazu klíčové.", + "Na konci bývá prosba: Call me back.", + NULL, + }, + }, +}; + +static const char *en_y1_l3_listen = "Hi Tom, it's Anna. Are you free on Saturday? I'm meeting Eva at the station at ten. We're going to the market first, and then we're having lunch in that new café by the river. In the afternoon I'm playing volleyball with my class in the park. Do you want to come to the café? Call me back tonight."; + +static const ChoiceQ en_y1_l3_listenq[] = { + {"Who is Anna meeting?", {"Tom", "Eva", "Her class", "Her mum"}, 4, 1}, + {"Where are they meeting?", {"At school", "At the station", "In the park", "By the river only"}, 4, 1}, + {"What is Anna doing in the afternoon?", {"Going to the market", "Having lunch", "Playing volleyball", "Sleeping"}, 4, 2}, + {"What should Tom do?", {"Buy tickets", "Call her back", "Come to school", "Bring a map"}, 4, 1}, +}; +static const char *en_y1_l3_listenq_hints[] = { + "I'm meeting Eva.", + "at the station at ten.", + "I'm playing volleyball.", + "Call me back tonight.", +}; + +static const TypedQ en_y1_l3_gaps[] = { + {"Listen, the phone ___.", "is ringing", "Děje se to teď."}, + {"We ___ lunch with Eva on Saturday.", "are having|'re having", "Domluvený plán."}, + {"I ___ not coming to the match.", "am|'m", "Zápor: am not."}, + {"She ___ volleyball this afternoon.", "is playing|'s playing", "Domluvená aktivita."}, +}; + +static const NetSlide en_y1_l4_slides[] = { + { + "1 / 2 • There is", "Co v pokoji je", "Tip: there is pro jedno, there are pro více. There je formální podmět.", + { + "There is a desk under the window. There are two chairs.", + "Zápor: There isn't a TV. There aren't any posters.", + "Otázka: Is there a lamp? Are there any books?", + "Předložky: in, on, under, next to, between, behind, in front of.", + NULL, + }, + }, + { + "2 / 2 • Psaní", "Popis místa", "Tip: jděte od velkého k malému, od dveří k oknu.", + { + "Začněte velikostí a světlem: small but bright.", + "Každá věta ať má věc a místo: The bag is under the chair.", + "Střídejte there is a there are, ať text není seznam.", + "Na konci jedna osobní věta: I like it because it is quiet.", + NULL, + }, + }, +}; + +static const char *en_y1_l4_write[] = { + "Describe your room in at least 30 words. Say what is in it and where things are. Use on and a piece of furniture such as a bed, and mention a window.", + "My room is small but bright. There is a bed next to the window and a wooden desk under a shelf. My books are on the desk and my school bag is under the chair. There are two posters on the wall. I like the room because it is quiet in the evening.", + "bed|window|on", + "30", +}; + +static const ChoiceQ en_y1_l4_quiz[] = { + {"The lamp is ___ the desk.", {"under", "on", "between", "at"}, 4, 1}, + {"There ___ a big window.", {"are", "is", "be", "have"}, 4, 1}, + {"There ___ two posters on the wall.", {"is", "are", "has", "been"}, 4, 1}, + {"The cat is sleeping ___ the bed.", {"under", "next", "between to", "at to"}, 4, 0}, +}; +static const char *en_y1_l4_quiz_hints[] = { + "Lampa stojí na desce: on.", + "Jedno okno: there is.", + "Dva plakáty: there are.", + "Pod postelí: under the bed.", +}; + +static const NetSlide en_y1_l5_slides[] = { + { + "1 / 2 • Množství", "Počitatelné a nepočitatelné", "Tip: some v kladné větě, any v otázce a záporu. Nabídka je výjimka: Would you like some tea?", + { + "Počitatelné: an apple, some apples. Nepočitatelné: some bread, some milk.", + "How many apples? How much milk?", + "a little milk, a few rolls, a lot of cheese.", + "V obchodě: How much is it? It's four euros a kilo.", + NULL, + }, + }, + { + "2 / 2 • Dialog", "Zdvořilá žádost", "Tip: Can I have… a I'd like… zní v obchodě přirozeně.", + { + "Have you got any bread? — Sorry, we haven't got any today.", + "We've got some rolls instead.", + "How many would you like? — Six, please.", + "Anything else? — No, that's all. Thank you.", + NULL, + }, + }, +}; + +static const char *en_y1_l5_read = "On Saturday morning the market by the river is busy. A woman asks, \"How much is this cheese?\" The seller says, \"It's four euros a kilo.\" Then she asks, \"Have you got any bread?\" He answers, \"Sorry, we haven't got any bread today, but we've got some fresh rolls.\" \"How many rolls do you want?\" \"Six, please, and a little milk.\" She also buys a few apples. \"Anything else?\" \"No, that's all.\""; + +static const ChoiceQ en_y1_l5_readq[] = { + {"How much is the cheese?", {"Four euros a piece", "Four euros a kilo", "Six euros", "A little"}, 4, 1}, + {"What hasn't the seller got?", {"Rolls", "Milk", "Bread", "Apples"}, 4, 2}, + {"How many rolls does the woman want?", {"Four", "A few", "Six", "A kilo"}, 4, 2}, + {"Which phrase is an offer of more shopping?", {"That's all.", "Anything else?", "How much is it?", "A little milk."}, 4, 1}, +}; +static const char *en_y1_l5_readq_hints[] = { + "four euros a kilo.", + "haven't got any bread today.", + "Six, please.", + "Anything else? nabízí další nákup.", +}; + +static const TypedQ en_y1_l5_gaps[] = { + {"We haven't got ___ bread today.", "any", "Zápor: any."}, + {"There are ___ rolls in the bag.", "some", "Kladná věta: some."}, + {"How ___ apples do you want?", "many", "Apples se dají počítat."}, + {"How ___ milk is in the bottle?", "much", "Milk je nepočitatelné."}, +}; + +static const NetSlide en_y1_l6_slides[] = { + { + "1 / 2 • Past simple", "Co se stalo a je dokončené", "Tip: zápor a otázka používají did, sloveso zůstává v základním tvaru.", + { + "Pravidelná: play → played, visit → visited, try → tried, stop → stopped.", + "Nepravidelná: go → went, take → took, swim → swam, buy → bought.", + "I didn't stay in a hotel. Did you visit the island?", + "Signály: yesterday, last July, ago, in 2019, when I was ten.", + NULL, + }, + }, + { + "2 / 2 • Vyprávění", "Jedna událost za druhou", "Tip: then, after that a one day drží příběh pohromadě.", + { + "Začněte kdy a kam: Last July we went to Croatia.", + "Pak doprava, počasí a jedna konkrétní příhoda.", + "Nepravidelná slovesa se neučí po jednom u testu, ale ve větě.", + "Na konci dojem: It was the best week of the year.", + NULL, + }, + }, +}; + +static const char *en_y1_l6_listen = "Last July we went to Croatia. We travelled by train and then by bus because the flight was too expensive. The weather was hot every day. I swam in the morning and my brother tried surfing. He fell off the board a lot, but he loved it. One day we visited a small island. We didn't stay in a hotel. We stayed with our uncle near the beach. I took hundreds of photos."; + +static const ChoiceQ en_y1_l6_listenq[] = { + {"Where did they go?", {"Italy", "Croatia", "Greece", "Spain"}, 4, 1}, + {"How did they travel?", {"By plane", "By train and bus", "By car", "By ferry only"}, 4, 1}, + {"What did the brother try?", {"Swimming", "Surfing", "Cooking", "Driving"}, 4, 1}, + {"Where did they stay?", {"In a hotel", "On the island", "With their uncle", "On the train"}, 4, 2}, +}; +static const char *en_y1_l6_listenq_hints[] = { + "went to Croatia.", + "by train and then by bus.", + "tried surfing.", + "stayed with our uncle.", +}; + +static const ChoiceQ en_y1_l6_quiz[] = { + {"go → ___", {"goed", "went", "gone", "going"}, 4, 1}, + {"I ___ a lot of photos.", {"taked", "took", "taken", "token"}, 4, 1}, + {"She ___ in the sea every morning.", {"swimmed", "swam", "swum", "swims"}, 4, 1}, + {"We ___ stay in a hotel.", {"don't", "didn't", "wasn't", "haven't"}, 4, 1}, +}; +static const char *en_y1_l6_quiz_hints[] = { + "go, went, gone.", + "take, took, taken.", + "Minulý tvar swim je swam.", + "Zápor minulého času: didn't + sloveso.", +}; + +static const NetSlide en_y1_l7_slides[] = { + { + "1 / 2 • Budoucnost", "Plán, nebo rozhodnutí teď", "Tip: going to má důkaz nebo plán. Will je rozhodnutí v tu chvíli nebo odhad.", + { + "We've got tickets. We're going to leave at six. To je plán.", + "Look at those clouds. It's going to rain. To je důkaz.", + "The phone is ringing. I'll answer it. Rozhodnutí teď.", + "I think I'll stay at home. Will jako názor, ne jistý plán.", + NULL, + }, + }, + { + "2 / 2 • Zpráva", "Neformální styl", "Tip: Hi, kontraktované tvary a na konci otázka kamarádovi.", + { + "Začněte Hi a jménem, ne Dear Sir.", + "Napište kdy, co a proč. Jedna věta s because stačí.", + "I'm going to… je v takové zprávě přirozené.", + "Konec: Want to come? Write back.", + NULL, + }, + }, +}; + +static const TypedQ en_y1_l7_gaps[] = { + {"Look at those clouds. It ___ rain.", "is going to|'s going to", "Je vidět důkaz: going to."}, + {"I think I ___ stay at home tonight.", "will|'ll", "Názor v tu chvíli: will."}, + {"The tickets are in my bag. We ___ leave at six.", "are going to|'re going to", "Už je to domluvené."}, + {"Don't worry. I ___ help you with the bags.", "will|'ll", "Rozhodnutí teď: will."}, +}; + +static const char *en_y1_l7_write[] = { + "Write a short message to a friend about next weekend. Use at least 25 words. Say what you are going to do, mention the weekend, and give a reason with because.", + "Hi Lena, next weekend I am going to visit my cousins in Pilsen. We are going to the cinema on Saturday because a new film is on. On Sunday we are going to cook lunch together. Do you want to come with me? Write back soon.", + "going|weekend|because", + "25", +}; + +static const NetSlide en_y1_l8_slides[] = { + { + "1 / 2 • Co test bere", "A2 v jedné sadě", "Tip: nejdřív otázky, pak text nebo nahrávku. U psaní počítejte slova.", + { + "Čtení: najděte v textu čas, místo a důvod, nehádejte z jednoho slova.", + "Poslech: jména a čísla si poznamenejte hned.", + "Gramatika: be, přítomné časy, some/any, minulý čas, will a going to.", + "Psaní: krátký souvislý text, ne jen odrážky.", + NULL, + }, + }, + { + "2 / 2 • Jak opravovat", "Nejčastější čechismy", "Tip: po kontrole si přečtěte nápovědy, i když je věta správně.", + { + "I am 16, ne I have 16 years.", + "She likes, ne she like.", + "Did you went je špatně. Did you go.", + "There is a book, ne It is a book on the table, když věc uvádíte.", + NULL, + }, + }, +}; + +static const char *en_y1_l8_read = "Klara lives in a flat with her parents and a small dog. On weekdays she walks to school because it is only ten minutes away. She loves art. Last Friday her class visited a gallery and she bought a poster. This Saturday she is meeting her cousin at the station. They are going to a concert in the evening."; + +static const ChoiceQ en_y1_l8_readq[] = { + {"How does Klara get to school?", {"By tram", "She walks", "Her dad drives", "By train"}, 4, 1}, + {"What did she buy at the gallery?", {"A dog", "A ticket", "A poster", "A flat"}, 4, 2}, + {"Who is she meeting on Saturday?", {"Her parents", "Her art teacher", "Her cousin", "The singer"}, 4, 2}, +}; +static const char *en_y1_l8_readq_hints[] = { + "she walks to school.", + "she bought a poster.", + "meeting her cousin.", +}; + +static const char *en_y1_l8_listen = "Hi, it's Marek. I missed you after school. I'm at the sports centre. The match starts at four, not at three, so don't hurry. Can you bring my blue jacket? It's on the chair in the kitchen. See you there."; + +static const ChoiceQ en_y1_l8_listenq[] = { + {"Where is Marek?", {"At school", "At the sports centre", "In the kitchen", "On the tram"}, 4, 1}, + {"What time does the match start?", {"At three", "At four", "At five", "At ten"}, 4, 1}, + {"What should his friend bring?", {"A ticket", "A blue jacket", "Lunch", "A poster"}, 4, 1}, +}; +static const char *en_y1_l8_listenq_hints[] = { + "at the sports centre.", + "starts at four, not at three.", + "bring my blue jacket.", +}; + +static const char *en_y1_l8_write[] = { + "Write at least 30 words about your family. Use is or are, and the words family and because.", + "My family is not very big. I live with my parents and my younger sister. She is twelve and she is funny. We get on well because we both like films. My dad is a driver and my mum is a nurse. I am happy at home.", + "family|because", + "30", +}; + +static const ChoiceQ en_y1_l8_quiz[] = { + {"She ___ coffee.", {"don't like", "doesn't like", "not like", "isn't like"}, 4, 1}, + {"There ___ any milk.", {"isn't", "aren't", "hasn't", "don't"}, 4, 0}, + {"Yesterday we ___ to the lake.", {"go", "went", "gone", "going"}, 4, 1}, + {"I've got the tickets. We ___ leave at noon.", {"will", "are going to", "did", "are"}, 4, 1}, +}; +static const char *en_y1_l8_quiz_hints[] = { + "3. osoba: doesn't like.", + "Milk je jednotné: there isn't.", + "Minulý tvar go je went.", + "Plán s důkazem: going to.", +}; + +static const NetSlide en_y2_l1_slides[] = { + { + "1 / 2 • Stupňování", "Porovnat dvě věci i celou skupinu", "Tip: kratší přídavná jména berou -er/-est, delší more/most.", + { + "cheap → cheaper → the cheapest. Expensive → more expensive → the most expensive.", + "Nepravidelné: good → better → the best, bad → worse → the worst, far → further → the furthest.", + "as … as: The bus is as fast as the train today.", + "than stojí po komparativu: This hostel is cheaper than the hotel.", + NULL, + }, + }, + { + "2 / 2 • Past continuous", "Děje, který probíhal, když se stalo něco jiného", "Tip: průběhový čas je pozadí, prostý je krátká událost.", + { + "I was reading when the train stopped.", + "While we were waiting, it started to rain.", + "Zápor: We weren't sleeping. Otázka: Were you driving?", + "Nepleťte si ho s minulým prostým: I walked home popisuje celou cestu.", + NULL, + }, + }, +}; + +static const TypedQ en_y2_l1_gaps[] = { + {"This ticket is ___ (cheap) than the night train.", "cheaper", "Krátké přídavné jméno: -er."}, + {"It was the ___ (bad) hotel in the town.", "worst", "bad, worse, the worst."}, + {"Prague is not as ___ as London.", "big", "as + základní tvar + as."}, + {"I ___ (read) when the bus suddenly stopped.", "was reading", "Pozadí: past continuous."}, + {"While we ___ (wait), a street musician started to play.", "were waiting", "While + průběhový děj."}, +}; + +static const ChoiceQ en_y2_l1_quiz[] = { + {"far → ___ → the furthest", {"farer", "further", "more far", "farthest only"}, 4, 1}, + {"Which sentence is correct?", {"This café is more cheap than that one.", "This café is cheaper than that one.", "This café is the cheaper than that one.", "This café is cheap than that one."}, 4, 1}, + {"She ___ a map when she missed the stop.", {"looked", "was looking", "looks", "is looking"}, 4, 1}, + {"good → better → ___", {"the goodest", "the better", "the best", "the most good"}, 4, 2}, +}; +static const char *en_y2_l1_quiz_hints[] = { + "far, further, the furthest. Farthest je taky možné, ale further je v učebnici první.", + "cheap → cheaper than.", + "Dělo se to v okamžiku, kdy zmeškala zastávku.", + "the best.", +}; + +static const NetSlide en_y2_l2_slides[] = { + { + "1 / 2 • Modální slovesa", "Rada, povinnost, zákaz", "Tip: must je silný pocit mluvčího, have to je pravidlo zvenku.", + { + "You should rest. You shouldn't go to school tomorrow. To je rada.", + "I must call the doctor. Mluvčí cítí, že to je nutné.", + "You have to show your insurance card. To chce ordinace.", + "Mustn't je zákaz. Don't have to znamená, že to není nutné.", + NULL, + }, + }, + { + "2 / 2 • Tělo a příznaky", "Jak to říct u přepážky", "Tip: I've got a sore throat zní přirozeněji než I have pain in throat.", + { + "a headache, a sore throat, a cough, a temperature, a rash.", + "It hurts when I swallow. I feel dizzy.", + "How long have you had it? — Since Monday. / For three days.", + "Take these tablets twice a day. Drink plenty of water.", + NULL, + }, + }, +}; + +static const char *en_y2_l2_read = "The school clinic is open from seven to three. If you feel ill, you should tell your teacher first. You have to bring your insurance card. You must not come to school with a high temperature. Students with a cough should wear a mask in the waiting room. You don't have to make an appointment for a small cut, but you should come before noon. The nurse can give you a painkiller. She can't give you antibiotics."; + +static const ChoiceQ en_y2_l2_readq[] = { + {"What do you have to bring?", {"A mask", "An insurance card", "Antibiotics", "An appointment"}, 4, 1}, + {"What is forbidden?", {"Coming with a high temperature", "Telling the teacher", "Wearing a mask", "Coming before noon"}, 4, 0}, + {"You don't have to make an appointment if…", {"you need antibiotics", "you have a small cut", "you have a temperature", "you arrive at four"}, 4, 1}, + {"What can't the nurse give you?", {"Water", "A painkiller", "Antibiotics", "A mask"}, 4, 2}, +}; +static const char *en_y2_l2_readq_hints[] = { + "have to bring your insurance card.", + "must not come with a high temperature.", + "for a small cut.", + "She can't give you antibiotics.", +}; + +static const ChoiceQ en_y2_l2_quiz[] = { + {"You ___ shout in the waiting room. It's a rule.", {"don't have to", "mustn't", "shouldn't to", "haven't"}, 4, 1}, + {"You ___ wear a tie. Nobody minds.", {"mustn't", "don't have to", "must", "should to"}, 4, 1}, + {"I've got a headache. You ___ drink some water and rest.", {"should", "must to", "have", "are"}, 4, 0}, + {"Visitors ___ show a card. The clinic asks for it.", {"have to", "should to", "don't must", "are have"}, 4, 0}, +}; +static const char *en_y2_l2_quiz_hints[] = { + "Zákaz: mustn't.", + "Není to nutné: don't have to.", + "Rada: should.", + "Pravidlo zvenku: have to.", +}; + +static const NetSlide en_y2_l3_slides[] = { + { + "1 / 2 • Present perfect", "Zkušenost a děj až do teď", "Tip: have/has + příčestí. Otázka na zkušenost začíná Have you ever…?", + { + "I have worked in a café. She has never had a paid job.", + "Ever v otázce, never v záporném významu: Have you ever used a till?", + "For three months = jak dlouho. Since May = od kdy.", + "Just, already, yet: I've just finished. I haven't finished yet.", + NULL, + }, + }, + { + "2 / 2 • Pohovor", "Krátké odpovědi", "Tip: odpovězte a přidejte jednu konkrétní větu, ať to není jen yes.", + { + "I'm reliable and I get on well with people.", + "I have looked after my neighbours' children.", + "I can work on Saturdays, but I can't work on Sunday mornings.", + "Could you tell me what the shift is?", + NULL, + }, + }, +}; + +static const char *en_y2_l3_listen = "Manager: Have you ever worked in a shop? Tereza: Not in a shop, but I have helped in my uncle's bakery since May. Manager: What do you do there? Tereza: I have served customers and I have learned to use the till. I haven't baked bread yet. Manager: Can you start next Saturday? Tereza: Yes. I have already asked my uncle, and he says it's fine. I can't work on Sunday mornings because I have a match."; + +static const ChoiceQ en_y2_l3_listenq[] = { + {"Where has Tereza helped?", {"In a supermarket", "In her uncle's bakery", "In a hotel", "At school"}, 4, 1}, + {"Since when?", {"Since March", "Since May", "Since Saturday", "For a year"}, 4, 1}, + {"What hasn't she done yet?", {"Served customers", "Used the till", "Baked bread", "Asked her uncle"}, 4, 2}, + {"Why can't she work on Sunday mornings?", {"The bakery is closed", "She has a match", "She is ill", "She has no till"}, 4, 1}, +}; +static const char *en_y2_l3_listenq_hints[] = { + "in my uncle's bakery.", + "since May.", + "I haven't baked bread yet.", + "because I have a match.", +}; + +static const TypedQ en_y2_l3_gaps[] = { + {"She ___ (live) here since 2019.", "has lived|'s lived", "Since + předpřítomný čas."}, + {"I have known him ___ three years.", "for", "For + délka."}, + {"Have you ___ (be) to London?", "been", "Been to = mít zkušenost."}, + {"We haven't finished ___.", "yet", "Yet v záporu na konci."}, + {"He has ___ eaten, so he isn't hungry.", "already|just", "Already nebo just."}, +}; + +static const NetSlide en_y2_l4_slides[] = { + { + "1 / 2 • Vztažné věty", "Která osoba, která věc", "Tip: who pro lidi, which pro věci. That jde často pro obojí v definující větě.", + { + "The actor who plays the captain is brilliant.", + "A lighthouse which stands on a black rock.", + "V definující větě se čárka nepíše: The boy who lives next door.", + "Nedefinující věta čárku má a that se v ní nepoužívá: Prague, which I love, …", + NULL, + }, + }, + { + "2 / 2 • Recenze", "Názor, ne jen děj", "Tip: neprozrazujte konec. Napište, pro koho film je.", + { + "Jedna věta o ději, jedna o postavě, jedna o tom, co se povedlo.", + "I would recommend it to anyone who likes quiet films.", + "The ending, which I will not describe, stays with you.", + "Hodnocení ať je konkrétní: the music is too loud, the script is sharp.", + NULL, + }, + }, +}; + +static const char *en_y2_l4_write[] = { + "Write a short review of a film or a book in at least 40 words. Use who once and which once, and say who you would recommend it to.", + "The Lighthouse Keeper is a quiet film which follows a woman on a small island. The actor who plays her hardly speaks, but her face tells the story. The sea, which is almost a character, is beautiful and dangerous. I would recommend it to anyone who likes slow films. The ending is sad but honest.", + "who|which|recommend", + "40", +}; + +static const ChoiceQ en_y2_l4_quiz[] = { + {"The nurse ___ helped me was very calm.", {"which", "who", "where", "whose it"}, 4, 1}, + {"The app ___ crashed is useless.", {"who", "which", "where", "whom"}, 4, 1}, + {"Prague, ___ is my home city, is full of tourists in July.", {"that", "which", "who", "what"}, 4, 1}, + {"That's the boy ___ dog ran into the road.", {"who", "which", "whose", "that his"}, 4, 2}, +}; +static const char *en_y2_l4_quiz_hints[] = { + "Člověk: who.", + "Věc: which.", + "S čárkou that nepatří. Which.", + "Čí: whose.", +}; + +static const NetSlide en_y2_l5_slides[] = { + { + "1 / 2 • Too / enough", "Příliš a dost", "Tip: too stojí před přídavným jménem, enough za ním.", + { + "It's too cold to swim. The water isn't warm enough.", + "Too many people, too much rain.", + "Enough + podstatné jméno: enough water. Nebo za přídavným jménem: old enough.", + "Not enough time je častější než too little time.", + NULL, + }, + }, + { + "2 / 2 • Předpověď", "Co říct, když se díváte z okna", "Tip: mraky, které vidíte, vedou k going to. Odhad bez důkazu k will.", + { + "Those clouds are black. It's going to storm.", + "I think it will be sunny on Sunday. To je jen odhad.", + "If it rains, we'll stay in the cabin.", + "Slovník: foggy, chilly, pouring, a heatwave, a breeze.", + NULL, + }, + }, +}; + +static const char *en_y2_l5_read = "We wanted to cycle to the lake, but the morning was too foggy to start. By ten the sun was out and the path was dry enough. There were too many cyclists near the dam, so we took a quieter road. We didn't have enough water, which was a mistake. At the lake a guard said, \"Look at that cloud. It's going to pour. I think the storm will last an hour.\" We were not fast enough. We got soaked."; + +static const ChoiceQ en_y2_l5_readq[] = { + {"Why didn't they start early?", {"The bikes were broken", "It was too foggy", "The lake was closed", "They had no map"}, 4, 1}, + {"Why did they change the road?", {"There were too many cyclists", "They were lost", "It was pouring", "The guard sent them back"}, 4, 0}, + {"What mistake did they make?", {"They forgot the bikes", "They didn't have enough water", "They started at night", "They swam"}, 4, 1}, + {"What did the guard predict from the cloud?", {"Fog", "A heatwave", "Rain", "Snow"}, 4, 2}, +}; +static const char *en_y2_l5_readq_hints[] = { + "too foggy to start.", + "too many cyclists near the dam.", + "didn't have enough water.", + "It's going to pour.", +}; + +static const TypedQ en_y2_l5_gaps[] = { + {"The rucksack is ___ heavy to carry.", "too", "Too + přídavné jméno."}, + {"She isn't tall ___ to reach the shelf.", "enough", "Enough za přídavným jménem."}, + {"There is too ___ sugar in this tea.", "much", "Nepočitatelné: too much."}, + {"Look at the sky. It ___ snow.", "is going to|'s going to", "Viditelný důkaz."}, +}; + +static const NetSlide en_y2_l6_slides[] = { + { + "1 / 2 • Dva časy", "Kdy je děj uzavřený", "Tip: jakmile zazní yesterday, last year nebo in 2019, patří minulý čas.", + { + "I have been to the festival three times. Neříkám kdy.", + "I went in 2023. Tehdy pršelo. To už je uzavřené.", + "Have you ever slept in a tent? — Yes. I slept in one last June.", + "Been to znamená, že už je člověk zpátky. Gone to znamená, že je pořád pryč.", + NULL, + }, + }, + { + "2 / 2 • Reportáž", "Co se děje na místě", "Tip: reportér míchá „už se stalo“ a „bylo to včera večer“.", + { + "The gates have just opened.", + "Last night the main band played for two hours.", + "Have you seen the lanterns yet?", + "A local baker has made a cake in the shape of a violin.", + NULL, + }, + }, +}; + +static const char *en_y2_l6_listen = "This is Nora at the river festival. The gates have just opened and hundreds of people have already come in. I have never seen so many lanterns. Last night the main band played for two hours, but tonight's singer hasn't arrived yet. A local baker has made a cake in the shape of a violin. I tried a piece yesterday. It was delicious. Have you ever been here? I came for the first time in 2022, and I have come back every summer since then."; + +static const ChoiceQ en_y2_l6_listenq[] = { + {"What has just happened?", {"The band has left", "The gates have opened", "It has started to snow", "The baker has closed"}, 4, 1}, + {"When did the main band play?", {"Just now", "Last night", "In 2022", "Every summer"}, 4, 1}, + {"What hasn't happened yet?", {"People have come in", "Tonight's singer has arrived", "Nora tried the cake", "The gates opened"}, 4, 1}, + {"When did Nora come for the first time?", {"Yesterday", "Last night", "In 2022", "This morning"}, 4, 2}, +}; +static const char *en_y2_l6_listenq_hints[] = { + "The gates have just opened.", + "Last night the main band played.", + "hasn't arrived yet.", + "for the first time in 2022.", +}; + +static const ChoiceQ en_y2_l6_quiz[] = { + {"I ___ this play in 2019.", {"have seen", "saw", "seen", "have saw"}, 4, 1}, + {"She has ___ to Paris. She is still there.", {"been", "gone", "went", "going"}, 4, 1}, + {"___ you ever slept in a tent?", {"Did", "Have", "Do", "Were"}, 4, 1}, + {"We ___ the lanterns yesterday.", {"have seen", "saw", "seen", "have watch"}, 4, 1}, +}; +static const char *en_y2_l6_quiz_hints[] = { + "Rok uzavírá děj: saw.", + "Pořád je pryč: gone.", + "Ever + present perfect: Have you ever…?", + "Yesterday → past simple.", +}; + +static const NetSlide en_y2_l7_slides[] = { + { + "1 / 2 • Podmínky", "Reálná a hypotetická", "Tip: v if větě není will. If it rains, we will stay in.", + { + "1. kondicionál: If I finish early, I will call you. Je to možné.", + "2. kondicionál: If I had a car, I would drive. Teď auto nemám.", + "V formální druhé podmínce je were: If I were you, I would apologise.", + "Unless = if not: We'll miss the bus unless we leave now.", + NULL, + }, + }, + { + "2 / 2 • Vlastní věta", "Jedna reálná, jedna vysněná", "Tip: would se v if větě druhé podmínky neopakuje na obou stranách špatně. If I would have je chyba.", + { + "Napište situaci, která se může stát tento týden.", + "Pak situaci, která teď pravda není.", + "Would like není druhá podmínka. To je jen přání: I'd like a tea.", + "Krátký text ať má oba typy, ať je rozdíl vidět.", + NULL, + }, + }, +}; + +static const TypedQ en_y2_l7_gaps[] = { + {"If it ___ (rain), we will stay in the tent.", "rains", "1. podmínka: přítomný čas v if."}, + {"If I ___ (be) you, I would tell the truth.", "were|was", "Were je přesnější, was se taky slyší."}, + {"She would travel more if she ___ (have) time.", "had", "2. podmínka: minulý tvar."}, + {"We'll be late ___ we hurry.", "unless", "Unless = if not."}, +}; + +static const char *en_y2_l7_write[] = { + "Write at least 35 words. Include one real plan with if and will, and one unreal dream with would. Use the word if twice.", + "If I finish my project tonight, I will go climbing with Eva on Saturday. We will take the early bus if the weather is fine. If I had more free time, I would learn the guitar. I would also travel by night train if tickets were cheaper. If is a small word with two very different lives.", + "if|will|would", + "35", +}; + +static const NetSlide en_y2_l8_slides[] = { + { + "1 / 2 • B1", "Co už má držet pohromadě", "Tip: u předpřítomného času hledejte ever, yet, since. U minulého yesterday a rok.", + { + "Stupňování, past continuous, modální slovesa rady a povinnosti.", + "Present perfect proti past simple.", + "Who a which, too a enough, první a druhá podmínka.", + "Psaní: krátký souvislý názor, ne odrážkový seznam.", + NULL, + }, + }, + { + "2 / 2 • Strategie", "Jedna věta navíc", "Tip: v psaní ať je důvod. Because nebo so text zvedne.", + { + "U čtení škrtněte odpověď, která v textu vůbec není.", + "U poslechu si čísla zapište číslicí.", + "Mustn't není don't have to.", + "If I would have time je chyba. If I had time, I would…", + NULL, + }, + }, +}; + +static const char *en_y2_l8_read = "The night bus to Brno is cheaper than the train, but it is also slower. Last Friday I was sleeping when the driver stopped at a dark petrol station. A woman who had missed her connection asked if she could sit next to me. She has travelled this route since March because her new job starts at six. If the bus is late, she has to run. She says she would move closer if flats were cheaper."; + +static const ChoiceQ en_y2_l8_readq[] = { + {"The bus is ___ than the train.", {"faster and cheaper", "cheaper and slower", "the best", "as fast"}, 4, 1}, + {"What was the writer doing when the bus stopped?", {"Reading", "Sleeping", "Running", "Driving"}, 4, 1}, + {"Why has the woman travelled this route since March?", {"She likes petrol stations", "Her job starts early", "Flats are cheap", "She missed a concert"}, 4, 1}, +}; +static const char *en_y2_l8_readq_hints[] = { + "cheaper, but slower.", + "I was sleeping when the driver stopped.", + "her new job starts at six.", +}; + +static const char *en_y2_l8_listen = "Doctor: How long have you had this cough? Adam: Since Sunday. I have also had a sore throat for two days. Doctor: You should rest and drink water. You don't have to stay in bed all day, but you mustn't play football this week. If you still feel ill on Friday, come back."; + +static const ChoiceQ en_y2_l8_listenq[] = { + {"Since when has Adam had the cough?", {"Since Friday", "Since Sunday", "For a month", "Since March"}, 4, 1}, + {"What mustn't he do?", {"Drink water", "Rest", "Play football", "Come back"}, 4, 2}, + {"When should he come back?", {"If he is still ill on Friday", "Every day", "Never", "On Sunday morning"}, 4, 0}, +}; +static const char *en_y2_l8_listenq_hints[] = { + "Since Sunday.", + "mustn't play football.", + "If you still feel ill on Friday.", +}; + +static const char *en_y2_l8_write[] = { + "Write at least 35 words about a trip or a job you would like. Use would and because.", + "I would like to work in a small bookshop by the station because I love quiet mornings. I would talk to customers and I would also read in the breaks. If I had the job, I would cycle there. I have never worked in a shop, but I have helped at school events.", + "would|because", + "35", +}; + +static const ChoiceQ en_y2_l8_quiz[] = { + {"This hostel is ___ than the hotel.", {"cheaper", "more cheap", "the cheapest", "cheap"}, 4, 0}, + {"I have lived here ___ 2020.", {"for", "since", "ago", "during"}, 4, 1}, + {"You ___ wear a helmet. It's the law.", {"must to", "have to", "don't have", "should to"}, 4, 1}, + {"If I ___ richer, I would take the train.", {"am", "will be", "were", "would be"}, 4, 2}, +}; +static const char *en_y2_l8_quiz_hints[] = { + "cheap → cheaper.", + "Since + bod v čase.", + "Pravidlo: have to.", + "2. podmínka: were.", +}; + +static const NetSlide en_y3_l1_slides[] = { + { + "1 / 2 • Passive", "Když je důležitější děj než původce", "Tip: be + příčestí. Čas nese sloveso be, ne příčestí.", + { + "English is spoken here. The window was broken last night.", + "Původce s by, jen když na něm záleží: The book was written by a nurse.", + "Modální trpný rod: The form must be signed. It can be recycled.", + "Nelze trpně vyjádřit každé sloveso. Happen trpný rod nemá.", + NULL, + }, + }, + { + "2 / 2 • Popis procesu", "Od suroviny k věci", "Tip: first, then, after that, finally. Původce často chybí, protože je obecný.", + { + "The beans are picked by hand. They are dried in the sun.", + "The jars were labelled yesterday.", + "Waste paper is collected every Thursday.", + "A new bridge is being built. To už je průběhový trpný rod, zatím stačí ho poznat.", + NULL, + }, + }, +}; + +static const TypedQ en_y3_l1_gaps[] = { + {"This bread ___ (bake) every morning.", "is baked", "Přítomný trpný rod."}, + {"The letters ___ (send) yesterday.", "were sent", "Minulý trpný rod, množné číslo."}, + {"The poem ___ (write) by a student.", "was written", "Write → written."}, + {"These bottles can ___ (recycle).", "be recycled", "Modální sloveso + be + příčestí."}, + {"The window ___ (not break) by the wind. Someone hit it.", "wasn't broken|was not broken", "Zápor minulého trpného rodu."}, +}; + +static const ChoiceQ en_y3_l1_quiz[] = { + {"Which sentence is passive?", {"Someone stole my bike.", "My bike was stolen.", "My bike stole.", "Someone was steal my bike."}, 4, 1}, + {"The news ___ at ten.", {"is reading", "are read", "is read", "read"}, 4, 2}, + {"You ___ wear a badge. The factory requires it, in the passive of must.", {"must be worn a badge", "A badge must be worn", "A badge must worn", "A badge is must wear"}, 4, 1}, + {"They built the school in 1998. → The school ___ in 1998.", {"built", "was built", "was build", "has built"}, 4, 1}, +}; +static const char *en_y3_l1_quiz_hints[] = { + "was + stolen.", + "News je jednotné: is read.", + "A badge must be worn.", + "was built.", +}; + +static const NetSlide en_y3_l2_slides[] = { + { + "1 / 2 • Reported speech", "Posun času", "Tip: said that, told me that. Tell potřebuje osobu, say ne.", + { + "„I am tired.“ → She said she was tired.", + "„I will help.“ → He said he would help.", + "„I have finished.“ → She told me she had finished.", + "„I can't come.“ → He said he couldn't come.", + NULL, + }, + }, + { + "2 / 2 • Kdy čas neposouvat", "Pořád to platí", "Tip: obecné pravdy a situace, které trvají, často zůstávají v přítomném čase.", + { + "The teacher said that water boils at 100 degrees.", + "She said she lives in Kolín, a pořád tam bydlí.", + "Zájmena se mění podle smyslu: „my book“ → he said his book.", + "Today → that day, tomorrow → the next day, yesterday → the day before. V testu to bude jen u jasných vět.", + NULL, + }, + }, +}; + +static const char *en_y3_l2_read = "Our head teacher told the local paper that the school was changing. She said that students would start an hour later next term. One pupil told the reporter that he was pleased because he hated early buses. A parent said that she was worried. She said her daughter had joined three clubs and had very little time. The cook said that lunch would still be at noon. He added that the menu was going to include more vegetables."; + +static const ChoiceQ en_y3_l2_readq[] = { + {"What did the head teacher say about the start of school?", {"It would start an hour later", "It would start at night", "It had already closed", "Clubs would end"}, 4, 0}, + {"Why was the pupil pleased?", {"He liked buses", "He hated early buses", "He wanted less lunch", "He had three clubs"}, 4, 1}, + {"What had the daughter done?", {"She had left school", "She had joined three clubs", "She had become a cook", "She had bought a bus"}, 4, 1}, + {"Lunch, according to the cook, …", {"would move to the evening", "would still be at noon", "had been cancelled", "was only vegetables"}, 4, 1}, +}; +static const char *en_y3_l2_readq_hints[] = { + "students would start an hour later.", + "because he hated early buses.", + "had joined three clubs.", + "would still be at noon.", +}; + +static const ChoiceQ en_y3_l2_quiz[] = { + {"„I am hungry.“ → She said she ___ hungry.", {"is", "was", "has", "were be"}, 4, 1}, + {"„I will call.“ → He said he ___ call.", {"will", "would", "called", "has"}, 4, 1}, + {"Which verb needs a person?", {"say", "tell", "speak that", "talk that"}, 4, 1}, + {"She said, „I can't swim.“ → She said she ___.", {"can't swim", "couldn't swim", "doesn't swim", "hasn't swum"}, 4, 1}, +}; +static const char *en_y3_l2_quiz_hints[] = { + "am → was.", + "will → would.", + "tell somebody.", + "can → could. V nepřímé řeči o minulém rozhovoru.", +}; + +static const NetSlide en_y3_l3_slides[] = { + { + "1 / 2 • Prostředí", "Konkrétní slova, ne jen nature", "Tip: drought, soil, volunteer, ban, shortcut. U maturitního tématu zabírají víc než slovo pollution samo.", + { + "A drought is a long time without rain. Soil is the earth plants grow in.", + "Unless it rains, the young trees will die.", + "If the town banned cars, the air would be cleaner. Teď je nezakázalo, proto would.", + "If people keep using the road, the soil will wash away. To se může stát.", + NULL, + }, + }, + { + "2 / 2 • Podcast", "Kdo co řekl", "Tip: u nepřímé řeči v poslechu si pamatujte sloveso řekl/řekla a pak obsah.", + { + "Ranger = strážce lesa.", + "Young trees were planted, but many may still die.", + "A shortcut šetří čas a někdy ničí místo.", + "Čísla si zapište: two thousand.", + NULL, + }, + }, +}; + +static const char *en_y3_l3_listen = "Last month our class recorded a podcast called The Last Beech. A ranger said that the trees were thirsty because the summers had become hotter. She told us that volunteers had planted two thousand young trees, but she added that many would die unless it rained. If the town banned cars from the forest road, the air would be cleaner. If people keep using it as a shortcut, the soil will wash away."; + +static const ChoiceQ en_y3_l3_listenq[] = { + {"What did the ranger say about the trees?", {"They were thirsty", "They had been cut down", "They were a shortcut", "They banned cars"}, 4, 0}, + {"How many young trees had volunteers planted?", {"Two hundred", "Two thousand", "Twenty", "None"}, 4, 1}, + {"The young trees will die unless…", {"people drive there", "it rains", "the podcast ends", "the soil is sold"}, 4, 1}, + {"What would make the air cleaner?", {"Banning cars from the forest road", "Using the road as a shortcut", "Cutting the beeches", "Washing the soil"}, 4, 0}, +}; +static const char *en_y3_l3_listenq_hints[] = { + "the trees were thirsty.", + "two thousand.", + "unless it rained / unless it rains. V textu: unless it rained v nepřímé řeči.", + "If the town banned cars.", +}; + +static const TypedQ en_y3_l3_gaps[] = { + {"If we ___ (ban) cars here, the air would be cleaner.", "banned", "2. podmínka."}, + {"The soil will wash away ___ people stop using the shortcut.", "unless", "Unless = if not. Pozor: věta chce if not ve významu dokud nepřestanou. Unless people stop = if they don't stop."}, + {"Many young trees ___ (plant) last month.", "were planted", "Minulý trpný rod."}, + {"She said the summers ___ (become) hotter.", "had become", "Posun předpřítomného času."}, +}; + +static const NetSlide en_y3_l4_slides[] = { + { + "1 / 2 • Čtyři kroky", "Úvod, dvě strany, závěr", "Tip: v úvodu otázku neopakujte doslova. Převeďte ji vlastními slovy.", + { + "Úvod: o čem text je a že existují dva pohledy.", + "Jeden odstavec pro výhody, jeden pro nevýhody.", + "On the one hand … On the other hand …", + "Závěr: váš názor. In my opinion, I believe. Nepřidávejte úplně nový argument.", + NULL, + }, + }, + { + "2 / 2 • Spojky", "However není because", "Tip: however stojí mezi větami a odděluje se čárkou. Although spojuje věty v jednu.", + { + "School uniforms are cheaper. However, they limit personality.", + "Although uniforms save time, many students hate them.", + "Because dává důvod. So dává výsledek.", + "In conclusion shrnuje. Nepíše se In the conclusion.", + NULL, + }, + }, +}; + +static const char *en_y3_l4_write[] = { + "Write a for-and-against paragraph of at least 50 words about school uniforms. Use however and because, and finish with your opinion using believe.", + "School uniforms are a common argument. They are practical because students do not have to decide what to wear, and families spend less money. However, a uniform hides personality and can feel childish at eighteen. Although it makes mornings faster, it does not make lessons better. I believe schools should keep a simple dress code, but they should not force one jacket on everyone.", + "however|because|believe", + "50", +}; + +static const ChoiceQ en_y3_l4_quiz[] = { + {"Which linker shows contrast?", {"because", "however", "so", "for example"}, 4, 1}, + {"___ uniforms save time, students still complain.", {"However", "Although", "So", "Believe"}, 4, 1}, + {"Choose the best ending of an essay.", {"In conclusion, I have no idea.", "In conclusion, I believe a simple rule is enough.", "Because however so.", "To begin with, the essay starts now."}, 4, 1}, + {"Because introduces…", {"a result only", "a reason", "a list of names", "a question"}, 4, 1}, +}; +static const char *en_y3_l4_quiz_hints[] = { + "However staví protiklad.", + "Although spojuje dvě části jedné věty.", + "Závěr má názor, ne nový zmatek.", + "Because = důvod.", +}; + +static const NetSlide en_y3_l5_slides[] = { + { + "1 / 2 • Dedukce", "Jisté, možné, vyloučené", "Tip: must je skoro jistota, might je možnost, can't je „to přece nejde“.", + { + "The lights are on. She must be at home.", + "He might be in the lab. I'm not sure.", + "That can't be Eva. Eva is in Vienna this week.", + "O minulosti: must have left, might have forgotten, can't have seen it.", + NULL, + }, + }, + { + "2 / 2 • Vynález", "Čtěte důkazy, ne nadpis", "Tip: u must have hledejte důkaz, který už nejde rozumně popřít.", + { + "A notebook with today's date is stronger than a guess.", + "Can't have znamená, že to odporuje faktu.", + "Might have nechává dvě možnosti otevřené.", + "Nepoužívejte can pro dedukci v kladné větě. Can umí, must musí být.", + NULL, + }, + }, +}; + +static const char *en_y3_l5_read = "Someone left a strange machine in the physics lab. It was warm, so it must have been switched on recently. The only key can't have been used by the caretaker, because he is in hospital. Two students, Nora and Adam, know the code. Nora might have come back after the club, but her train ticket shows she was in Pardubice. Adam's notebook is on the bench and the handwriting matches the label. He must have built it."; + +static const ChoiceQ en_y3_l5_readq[] = { + {"Why must the machine have been on recently?", {"It was noisy", "It was warm", "It was new", "It was labelled"}, 4, 1}, + {"Why can't the caretaker have used the key?", {"He lost it", "He is in hospital", "He hates physics", "He was in Pardubice"}, 4, 1}, + {"Where was Nora?", {"In the lab", "In Pardubice", "In hospital", "On the bench"}, 4, 1}, + {"Who must have built the machine?", {"The caretaker", "Nora", "Adam", "Nobody"}, 4, 2}, +}; +static const char *en_y3_l5_readq_hints[] = { + "It was warm.", + "he is in hospital.", + "her ticket shows Pardubice.", + "The notebook and the handwriting point to Adam.", +}; + +static const ChoiceQ en_y3_l5_quiz[] = { + {"The window is broken and his bag is gone. He ___ left in a hurry.", {"must have", "must", "can have", "should"}, 4, 0}, + {"That ___ be Lea. Lea is abroad.", {"must", "can't", "should", "is"}, 4, 1}, + {"I'm not sure. They ___ have missed the bus.", {"must", "can't", "might", "are"}, 4, 2}, + {"Which sentence is a deduction, not a duty?", {"You must wear gloves.", "She must be tired.", "You must sign this.", "Visitors must wait."}, 4, 1}, +}; +static const char *en_y3_l5_quiz_hints[] = { + "Minulá skoro jistota: must have left.", + "Odporuje faktu: can't.", + "Možnost: might have.", + "She must be tired je úsudek.", +}; + +static const NetSlide en_y3_l6_slides[] = { + { + "1 / 2 • Reported questions", "Otázka se v nepřímé řeči narovná", "Tip: žádný otazník a pořadí podmětu a slovesa jako v oznamovací větě.", + { + "„Where do you live?“ → She asked me where I lived.", + "„Did you see it?“ → He asked if I had seen it.", + "Whether a if jsou u zjišťovacích otázek obě možné.", + "Tázací výraz zůstává: what, where, why, how long.", + NULL, + }, + }, + { + "2 / 2 • Redakce", "Novinář se ptá starosty", "Tip: v poslechu odlište otázku reportéra od odpovědi starosty.", + { + "Asked if the bridge was safe.", + "Wondered how long the repairs would take.", + "The mayor said he didn't know yet.", + "Slovesa ask, wonder, want to know.", + NULL, + }, + }, +}; + +static const char *en_y3_l6_listen = "Reporter: Mayor, is the old bridge safe? Mayor: I can't promise that. An engineer told me that two cables were weak. Reporter: How long will the repairs take? Mayor: She asked the firm the same question yesterday. They said they didn't know yet. Reporter: Did anyone complain? Mayor: A shopkeeper asked me why the bus stop had moved. I asked her if she could wait until Friday. Reporter: Will you close the bridge? Mayor: We might. I have asked the police whether they can send officers tonight."; + +static const ChoiceQ en_y3_l6_listenq[] = { + {"What did the engineer tell the mayor?", {"The bridge was new", "Two cables were weak", "The shop was closed", "Friday was impossible"}, 4, 1}, + {"What didn't the firm know?", {"The mayor's name", "How long the repairs would take", "Where the bus stop was", "Who the police were"}, 4, 1}, + {"What did the shopkeeper ask?", {"Why the bus stop had moved", "If the mayor could swim", "Where the cables were made", "Whether Friday was a holiday"}, 4, 0}, + {"Who might come tonight?", {"The firm", "The shopkeeper", "Police officers", "The engineer only"}, 4, 2}, +}; +static const char *en_y3_l6_listenq_hints[] = { + "two cables were weak.", + "They said they didn't know yet. Šlo o délku oprav.", + "why the bus stop had moved.", + "whether they can send officers tonight.", +}; + +static const ChoiceQ en_y3_l6_quiz[] = { + {"„Where do you live?“ → He asked me where I ___.", {"did live", "lived", "do live", "living"}, 4, 1}, + {"„Are you ready?“ → She asked if I ___.", {"was ready", "am I ready", "ready was", "did ready"}, 4, 0}, + {"Which sentence is wrong?", {"He asked where she was.", "He asked where was she.", "He asked if she was tired.", "He wanted to know why she had left."}, 4, 1}, + {"Whether means about the same as…", {"because", "if", "however", "whose"}, 4, 1}, +}; +static const char *en_y3_l6_quiz_hints[] = { + "Pořadí oznamovací věty: where I lived.", + "if I was ready.", + "V nepřímé otázce není inverze.", + "Whether ≈ if v nepřímé zjišťovací otázce.", +}; + +static const NetSlide en_y3_l7_slides[] = { + { + "1 / 2 • Částice mění význam", "Look není look after", "Tip: frázové sloveso se učte jako jednu jednotku, s českým protějškem ve větě.", + { + "look after = starat se, look for = hledat, look forward to = těšit se.", + "give up = vzdát se, find out = zjistit, run out of = dojít zásoba.", + "turn off = vypnout, take off = sundat nebo vzlétnout, get on = nastoupit i vycházet.", + "Look forward to je následované -ing: I look forward to seeing you.", + NULL, + }, + }, + { + "2 / 2 • Příběh", "Jedno sloveso, jedna situace", "Tip: když částici neznáte, představte si gesto. Off je pryč, up je do konce.", + { + "The bus took off není o oblečení. U letadla ano, u autobusu spíš pulled away.", + "Take off a coat = sundat. Take off = vzlétnout. Kontext rozhoduje.", + "Get on with somebody = vycházet s někým.", + "Run out of time je u testu častější než run out time.", + NULL, + }, + }, +}; + +static const TypedQ en_y3_l7_gaps[] = { + {"Can you look ___ my dog this weekend?", "after", "Look after = postarat se."}, + {"We are looking ___ to the trip.", "forward", "Look forward to."}, + {"Don't give ___. The last chapter is the best.", "up", "Give up = vzdát to."}, + {"I need to find ___ who left the note.", "out", "Find out = zjistit."}, + {"We've run ___ of milk.", "out", "Run out of."}, +}; + +static const ChoiceQ en_y3_l7_quiz[] = { + {"Please turn ___ the lights.", {"off", "after", "forward", "out of"}, 4, 0}, + {"She gets ___ well with her neighbours.", {"on", "off", "up", "after"}, 4, 0}, + {"He took ___ his wet coat.", {"off", "up to", "after", "forward"}, 4, 0}, + {"I look forward to ___ you.", {"see", "seeing", "saw", "seen"}, 4, 1}, +}; +static const char *en_y3_l7_quiz_hints[] = { + "Turn off = vypnout.", + "Get on with = vycházet.", + "Sundat: take off.", + "To je předložka, následuje -ing.", +}; + +static const NetSlide en_y3_l8_slides[] = { + { + "1 / 2 • B1+", "Trpný rod, řeč, podmínky, dedukce", "Tip: u trpného rodu hledejte be. U nepřímé otázky žádnou inverzi.", + { + "was written, must be signed, is spoken.", + "said she would, asked where I lived.", + "If they banned cars, the air would be cleaner.", + "must have, might have, can't have.", + NULL, + }, + }, + { + "2 / 2 • Úvaha ve zkratce", "However a because v jednom textu", "Tip: jedna výhoda, jeden háček, jedna věta s názorem.", + { + "Neopakujte zadání první větou.", + "Phrasal verbs nechte ve správné částici. Look after není look for.", + "Told me, ne said me.", + "In conclusion je až na konci, ne v úvodu.", + NULL, + }, + }, +}; + +static const char *en_y3_l8_read = "A repair café opened in a former bakery. Broken radios are mended there on Saturdays. The founder said that people would rather repair than buy if they were shown how. She told a reporter that twenty kettles had been saved in March. Visitors have to bring the broken object, but they don't have to pay. If the café closed, a lot of working parts would be thrown away."; + +static const ChoiceQ en_y3_l8_readq[] = { + {"What happens to radios there?", {"They are sold", "They are mended", "They are banned", "They are thrown away at once"}, 4, 1}, + {"How many kettles had been saved in March?", {"Twelve", "Twenty", "Two thousand", "None"}, 4, 1}, + {"What would happen if the café closed?", {"Parts would be thrown away", "Radios would be cheaper", "People would pay more", "The bakery would reopen as a shop"}, 4, 0}, +}; +static const char *en_y3_l8_readq_hints[] = { + "are mended.", + "twenty kettles.", + "working parts would be thrown away.", +}; + +static const char *en_y3_l8_listen = "Editor: Did the mayor answer you? Journalist: I asked him whether the bridge was safe. He said he didn't know. I also asked how long the repairs would take. He told me that the firm hadn't decided. A resident asked me why the paper hadn't printed a map. I said we would print one tomorrow."; + +static const ChoiceQ en_y3_l8_listenq[] = { + {"What did the journalist ask about safety?", {"Whether the bridge was safe", "If the paper was closed", "Where the mayor lived", "Why kettles break"}, 4, 0}, + {"What hadn't the firm done?", {"Decided", "Printed a map", "Closed the paper", "Asked the resident"}, 4, 0}, + {"What did the journalist promise?", {"To close the bridge", "To print a map tomorrow", "To repair the cables", "To interview a baker"}, 4, 1}, +}; +static const char *en_y3_l8_listenq_hints[] = { + "whether the bridge was safe.", + "hadn't decided.", + "we would print one tomorrow.", +}; + +static const char *en_y3_l8_write[] = { + "In at least 40 words, give one reason for repairing things and one objection. Use because and however.", + "Repairing things matters because a kettle can work for years after a small fix. However, some people do not have the tools or the time, so a café where volunteers help is a fair idea. I believe every town should have one quiet room for broken objects. It is slower than shopping, but it is kinder.", + "because|however", + "40", +}; + +static const ChoiceQ en_y3_l8_quiz[] = { + {"The cake ___ by a student.", {"baked", "was baked", "was bake", "has bake"}, 4, 1}, + {"She asked me where I ___.", {"did live", "lived", "do live", "living"}, 4, 1}, + {"He ___ be at school. I just saw him there.", {"can't", "must", "mustn't", "should to"}, 4, 1}, + {"Please look ___ the tickets. I can't find them.", {"for", "forward", "after", "up of"}, 4, 0}, +}; +static const char *en_y3_l8_quiz_hints[] = { + "was baked.", + "where I lived.", + "Skoro jistota: must.", + "Look for = hledat. Look after by bylo hlídat.", +}; + +static const NetSlide en_y4_l1_slides[] = { + { + "1 / 2 • Ústní zkouška", "Čtyři minuty nejsou román", "Tip: katalog CERMAT chce souvislý projev, popis obrázku, porovnání a reakci na otázky. Ne seznam slov.", + { + "Úvod jednou větou: I'd like to talk about my town, which is smaller than it looks.", + "Obecné plus osobní: Many people commute. I cycle, because the centre is close.", + "Když slovo chybí, opis: the thing you use to open a tin.", + "Na obrázku: In the foreground, in the background, it looks as if.", + NULL, + }, + }, + { + "2 / 2 • Kolokace", "Make, do, have, take", "Tip: české „dělat“ se rozpadá. Do homework, make a mistake, take a photo, have a rest.", + { + "Make a decision, make friends, make the bed.", + "Do the washing-up, do a course, do someone a favour.", + "Take part in, take care of, take an exam.", + "Have a look, have a word with, have trouble with.", + NULL, + }, + }, +}; + +static const TypedQ en_y4_l1_gaps[] = { + {"I need to ___ a decision before Friday.", "make", "Make a decision."}, + {"She ___ her homework on the train.", "does|did", "Do homework."}, + {"Can I ___ a photo of the bridge?", "take", "Take a photo."}, + {"Let's ___ a rest. We've walked for hours.", "have", "Have a rest."}, + {"He ___ part in the school play every year.", "takes|took", "Take part in."}, +}; + +static const ChoiceQ en_y4_l1_quiz[] = { + {"You don't know the word kettle. What helps?", {"Stop the exam", "Paraphrase it", "Switch to Czech for the rest", "Invent a Latin word and insist"}, 4, 1}, + {"In the foreground means…", {"at the back", "at the front of the picture", "outside the frame", "in the past"}, 4, 1}, + {"Which collocation is right?", {"do a mistake", "make a mistake", "take a mistake", "have a mistake"}, 4, 1}, + {"A strong speaking answer…", {"is a list of single words", "adds a personal example", "translates the question", "avoids all verbs"}, 4, 1}, +}; +static const char *en_y4_l1_quiz_hints[] = { + "Opis je lepší než ticho.", + "Foreground = popředí.", + "Make a mistake.", + "Obecné plus vlastní příklad.", +}; + +static const NetSlide en_y4_l2_slides[] = { + { + "1 / 2 • Kostra", "Komu, proč, co chcete, jak se loučíte", "Tip: neznámé jméno → Dear Sir or Madam a Yours faithfully. Známé jméno → Dear Ms Novák a Yours sincerely.", + { + "I am writing to apply for… / to complain about…", + "I would be grateful if you could…", + "Kontrakce v čistě formálním textu omezte: I am, not I'm.", + "Na konci celé jméno. Žádné Cheers ani xoxo.", + NULL, + }, + }, + { + "2 / 2 • Stížnost", "Fakta, dopad, žádost", "Tip: zlost text oslabuje. Datum, číslo spoje a klidná žádost ho posílí.", + { + "The train on 3 May, which I had booked, arrived 90 minutes late.", + "As a result I missed the interview.", + "I would like a refund or an explanation.", + "I look forward to your reply.", + NULL, + }, + }, +}; + +static const char *en_y4_l2_write[] = { + "Write a formal email of at least 55 words to a railway company. Complain about a delay. Start with Dear, use would, and end with sincerely or faithfully.", + "Dear Sir or Madam, I am writing to complain about the night train to Berlin on 3 May. It arrived ninety minutes late, so I missed a morning interview. I would be grateful if you could explain the delay and offer a refund. I have attached my ticket. Please reply to this address. Yours faithfully, Tereza Malá.", + "dear|would|faithfully", + "55", +}; + +static const ChoiceQ en_y4_l2_quiz[] = { + {"You do not know the person's name. You end with…", {"Yours faithfully", "Yours sincerely", "Cheers", "Love"}, 4, 0}, + {"Dear Ms Novák, … ___", {"Yours faithfully", "Yours sincerely", "Hi again", "Bye"}, 4, 1}, + {"Which opening is formal?", {"Hey!!!", "I am writing to apply for the post.", "I wanna the job.", "What's up"}, 4, 1}, + {"A formal request sounds like…", {"Give me a refund now.", "I would be grateful if you could look into this.", "You guys messed up.", "Send money pls."}, 4, 1}, +}; +static const char *en_y4_l2_quiz_hints[] = { + "Neznámé jméno: faithfully. Po Dear Sir or Madam.", + "Jméno znáte: sincerely.", + "I am writing to…", + "I would be grateful if you could…", +}; + +static const NetSlide en_y4_l3_slides[] = { + { + "1 / 2 • Čtení u maturity", "Nečtěte každé slovo stejně pomalu", "Tip: u výběru ze čtyř možností nejdřív zjistěte, na co se otázka ptá, a teprve pak hledejte odstavec.", + { + "Hlavní myšlenka bývá v úvodu nebo v závěru, ne v jednom příkladu.", + "Distraktor často opakuje slovo z textu, ale v jiném vztahu.", + "Which surprises… uvádí postoj, ne jen fakt o ceně.", + "Not given v tomhle cvičení není. Jedna možnost je opřená o text.", + NULL, + }, + }, + { + "2 / 2 • Slovník z kontextu", "Boarding gate a carriage", "Tip: neznámé slovo ohraničte větou před ním a za ním.", + { + "A boarding gate je letištní východ k letadlu. Text ho staví proti posteli ve vlaku.", + "A carriage je vůz vlaku.", + "Demand = poptávka. If demand stays high, přidají vozy.", + "Deleted journey by byl let, který člověk chce mít co nejkratší.", + NULL, + }, + }, +}; + +static const char *en_y4_l3_read = "When the night train from Prague to Berlin returned last spring, stations filled with people who prefer a bed to a boarding gate. A single ticket is often more expensive than a flight, which surprises anyone who thinks trains are always the cheap choice. Still, passengers say they arrive calmer. They can read, talk and avoid the bright shops of an airport. Critics point out that a delay of two hours is common and that the cabins are small. The railway has promised that new carriages will be added if demand stays high. A student I spoke to had taken the train three times. She said she would fly only if she missed the evening departure. For her, the journey is part of the trip, not a problem to be deleted."; + +static const ChoiceQ en_y4_l3_readq[] = { + {"What is the main point?", {"Flights have been cancelled", "Some people choose the night train even when it costs more", "Berlin has closed its station", "Students may not travel"}, 4, 1}, + {"What surprises some readers?", {"That a train ticket can cost more than a flight", "That stations are empty", "That nobody reads", "That the train is free"}, 4, 0}, + {"What do critics mention?", {"Free meals", "Delays and small cabins", "Too many books", "A lack of stations"}, 4, 1}, + {"The student would fly only if…", {"the ticket was cheap", "she missed the evening departure", "the cabins were small", "she hated reading"}, 4, 1}, + {"New carriages will be added if…", {"critics agree", "demand stays high", "flights are banned", "students complain"}, 4, 1}, +}; +static const char *en_y4_l3_readq_hints[] = { + "Lístek bývá dražší než let, a přesto lidé vlak volí.", + "more expensive than a flight, which surprises…", + "a delay of two hours, cabins are small.", + "only if she missed the evening departure.", + "if demand stays high.", +}; + +static const NetSlide en_y4_l4_slides[] = { + { + "1 / 2 • Poslech u maturity", "Dvě přehrání stačí, když víte co", "Tip: při prvním poslechu chyťte kostru, při druhém doplňte past, podmínku a zápor.", + { + "Otázky si přečtěte dřív. Podtrhněte tázací slovo.", + "Čísla a never/already mění význam celé věty.", + "Wouldn't force není couldn't.", + "I didn't give him any medicine je důležité právě proto, že je zápor.", + NULL, + }, + }, + { + "2 / 2 • Práce s dětmi", "Slovník pohovoru", "Tip: deal with an emergency, refuse, offer, look forward to.", + { + "For two years je délka, ne bod v čase.", + "A quieter task je alternativa, ne trest.", + "Although přiznává nevýhodu a názor nemění.", + "Boots jsou důkaz, že s deštěm počítá. Proto going to / already.", + NULL, + }, + }, +}; + +static const char *en_y4_l4_listen = "Interviewer: Why did you apply to the summer camp in Wales? Hana: I have worked with children for two years, and I wanted a job where English is the only language. Interviewer: What would you do if a child refused to join an activity? Hana: I wouldn't force them. I would ask why, and I might offer a quieter task. Interviewer: Have you ever dealt with an emergency? Hana: Yes. Last August a boy cut his foot. I cleaned the cut and called the nurse. I didn't give him any medicine. Interviewer: The camp is rainy. Is that a problem? Hana: Not really. I have already bought boots. I look forward to the hills, although I know the work will be tiring."; + +static const ChoiceQ en_y4_l4_listenq[] = { + {"Why does Hana want the job?", {"She has never met children", "She wants a job where only English is spoken", "She hates hills", "She wants to sell boots"}, 4, 1}, + {"How long has she worked with children?", {"For two weeks", "For two years", "Since last August only", "She hasn't"}, 4, 1}, + {"What would she do if a child refused an activity?", {"Force them", "Send them home", "Ask why and maybe offer a quieter task", "Give them medicine"}, 4, 2}, + {"What didn't she do in the emergency?", {"Clean the cut", "Call the nurse", "Give medicine", "Help the boy"}, 4, 2}, + {"How does she feel about the rain?", {"She has already bought boots and still looks forward to the hills", "She will refuse the job", "She has never seen rain", "She hates children"}, 4, 0}, +}; +static const char *en_y4_l4_listenq_hints[] = { + "where English is the only language.", + "for two years.", + "I wouldn't force them. I might offer a quieter task.", + "I didn't give him any medicine.", + "already bought boots, look forward to the hills.", +}; + +static const NetSlide en_y4_l5_slides[] = { + { + "1 / 2 • Názor", "Nejdřív stanovisko, pak důvody", "Tip: u názorové eseje čtenář nemá hádat, na čí straně jste. Řekněte to v úvodu.", + { + "I believe that… / In my opinion…", + "First reason, second reason, a short answer to the other side.", + "Although some people argue that…, I still think…", + "Závěr ať zní jako závěr, ne jako třetí nový důvod.", + NULL, + }, + }, + { + "2 / 2 • Rozsah", "Radši čtyři hutné věty než deset prázdných", "Tip: u písemné práce se počítají slova. Opakování zadání se nepočítá jako myšlenka.", + { + "Každý důvod ať má příklad z běžného života.", + "Nepoužívejte etc. Místo toho jedno konkrétní slovo.", + "Spojky: firstly, in addition, on the other hand, in conclusion.", + "Kontraktované tvary jsou v názorové eseji v pořádku, když text není stížnost úřadu.", + NULL, + }, + }, +}; + +static const char *en_y4_l5_write[] = { + "Write an opinion of at least 60 words: should students have a gap year before university? Use believe, although and in conclusion.", + "I believe a gap year can help, but only when it has a plan. Although some students waste the time, others work, travel or look after a relative and return calmer. A year in a job also shows whether a degree is really necessary. On the other hand, a long break can make it harder to study again. In conclusion, a gap year is useful if the person can explain what they will do with it.", + "believe|although|conclusion", + "60", +}; + +static const ChoiceQ en_y4_l5_quiz[] = { + {"Where should your opinion appear in this kind of essay?", {"Only in a footnote", "In the introduction and again at the end", "Never", "Only in the title"}, 4, 1}, + {"Although introduces…", {"a concession", "a list of books", "a greeting", "a date"}, 4, 0}, + {"In conclusion should…", {"start a brand-new topic", "sum up", "replace the introduction", "ask the examiner a question"}, 4, 1}, + {"Which is the best topic sentence?", {"Gap year gap year gap year.", "A gap year is worth it when it has a clear plan.", "Hello my name is and I will write.", "Etc. etc. etc."}, 4, 1}, +}; +static const char *en_y4_l5_quiz_hints[] = { + "Úvod i závěr.", + "Ústupka: something true that does not win.", + "Shrnutí.", + "Věta, která už nese názor.", +}; + +static const NetSlide en_y4_l6_slides[] = { + { + "1 / 2 • Okruhy", "Ne izolovaná slovíčka", "Tip: ústní zkouška sahá na osobu, rodinu, domov, školu, volný čas, kulturu, sport, cestování, zdraví, jídlo, služby, práci, společnost a přírodu.", + { + "Heavy rain, strong coffee, fast food, junk food. Ne strong rain.", + "A balanced diet, processed food, a food allergy.", + "Public transport, rush hour, a traffic jam, a return ticket.", + "Renewable energy, a carbon footprint, a heatwave, a drought.", + NULL, + }, + }, + { + "2 / 2 • Společnost a práce", "Fráze, které unesou odstavec", "Tip: apply for a job, hand in a notice, earn a living, from home.", + { + "A gap year, an apprenticeship, a degree, tuition fees.", + "Freedom of speech, a stereotype, volunteer work.", + "Break the law, pay a fine, a witness.", + "Mental health, a check-up, be in pain. Ne have pain in the leg jako jediná možnost: My leg hurts.", + NULL, + }, + }, +}; + +static const TypedQ en_y4_l6_gaps[] = { + {"There was heavy ___ all night.", "rain", "Heavy rain, ne strong rain."}, + {"I'd like a ___ ticket to Brno.", "return", "Return ticket = zpáteční."}, + {"She applied ___ the post of guide.", "for", "Apply for."}, + {"Solar power is ___ energy.", "renewable", "Renewable energy."}, + {"He has a nut ___, so he can't eat the cake.", "allergy", "A food allergy."}, +}; + +static const ChoiceQ en_y4_l6_quiz[] = { + {"Which pair is right?", {"strong rain / heavy coffee", "heavy rain / strong coffee", "fast coffee / junk rain", "a traffic diet"}, 4, 1}, + {"Rush hour is…", {"a quiet morning", "the busiest time to travel", "a kind of ticket", "a school subject"}, 4, 1}, + {"An apprenticeship is…", {"a fine", "training for a job", "a heatwave", "a stereotype"}, 4, 1}, + {"My leg ___.", {"hurts", "is pain", "has hurted", "does hurtness"}, 4, 0}, +}; +static const char *en_y4_l6_quiz_hints[] = { + "Heavy rain, strong coffee.", + "Špička.", + "Praktická příprava na řemeslo nebo práci.", + "My leg hurts.", +}; + +static const NetSlide en_y4_l7_slides[] = { + { + "1 / 2 • Články a časy", "To, co se v testu sype", "Tip: the night train, a night train, night trains. Člen podle toho, jestli čtenář ví který.", + { + "I have lived here since May. I lived there in 2018.", + "If I were you, I would wait. If it rains, we will wait.", + "The book was written in 1962. People speak English here → English is spoken here.", + "Used to: I used to walk. Zvyk, který už neplatí. Didn't use to.", + NULL, + }, + }, + { + "2 / 2 • Věta", "Slovosled a shoda", "Tip: never dávejte před sloveso, ale za am/is/are: She is never late. She never arrives late.", + { + "A little time, a few minutes. Much v záporu a otázce, a lot of v kladné větě je bezpečné.", + "Suggest + -ing nebo suggest that. Suggest me to go je chyba.", + "Enjoy, avoid, mind, can't help + -ing.", + "Want, decide, promise, hope + to.", + NULL, + }, + }, +}; + +static const TypedQ en_y4_l7_gaps[] = { + {"I ___ (live) in this flat since May.", "have lived|'ve lived", "Since → present perfect."}, + {"She suggested ___ (take) the night train.", "taking", "Suggest + -ing."}, + {"If I ___ you, I would book earlier.", "were|was", "If I were you."}, + {"These letters must ___ (sign) today.", "be signed", "Must be + příčestí."}, + {"I ___ to get up at five. I don't anymore.", "used", "Used to."}, +}; + +static const ChoiceQ en_y4_l7_quiz[] = { + {"She enjoys ___.", {"to swim", "swimming", "swim", "swam"}, 4, 1}, + {"I have ___ finished.", {"yet", "just", "since", "ago"}, 4, 1}, + {"English ___ all over the building.", {"speaks", "is spoken", "is spoke", "speak"}, 4, 1}, + {"There isn't ___ time.", {"many", "much", "a few", "several"}, 4, 1}, + {"He asked me where ___.", {"did I live", "I lived", "do I lived", "lived I"}, 4, 1}, + {"You ___ shout. The baby is asleep. It is forbidden.", {"don't have to", "mustn't", "might", "used to"}, 4, 1}, +}; +static const char *en_y4_l7_quiz_hints[] = { + "Enjoy + -ing.", + "Just before the participle. Yet belongs at the end in negatives.", + "Trpný rod.", + "Time je nepočitatelné: much.", + "Bez inverze.", + "Zákaz: mustn't.", +}; + +static const NetSlide en_y4_l8_slides[] = { + { + "1 / 2 • Celá zkouška", "Tři části, jeden mozek", "Tip: didaktický test míchá poslech, čtení a jazykovou kompetenci. Písemná práce je zvlášť. Tady je zmenšený model.", + { + "Nejdřív přečtěte zadání psaní, ať víte, kolik času si necháte.", + "U čtení nehledejte stejná slova. Hledejte stejný význam.", + "U poslechu zápor bývá ta správná odpověď.", + "Když váháte mezi dvěma, vyhoďte tu, která přidává informaci v textu neslyšenou.", + NULL, + }, + }, + { + "2 / 2 • Písemná práce", "Účel, čtenář, rozsah", "Tip: e-mail kamarádovi a stížnost úřadu se nepíšou stejným jazykem. Tady jde o kamaráda.", + { + "Řekněte proč píšete hned v první větě.", + "Dejte tam čas a místo, ať text není mlha.", + "Jedna věta s because a jedna s if textu pomůžou.", + "Na konci otázka, která zní jako opravdový zájem.", + NULL, + }, + }, +}; + +static const char *en_y4_l8_read = "The town library now lends tools as well as books. Hammers, sewing machines and a cake tin can be borrowed for a week. The idea came from a librarian who was tired of people buying things they would use once. She said the workshop had been visited by more teenagers than the reading room. Critics argue that a library should stay quiet. She replies that a library should be useful. If the experiment works, other branches will copy it. Users have to return each object clean, and they must not lend it to anyone else."; + +static const ChoiceQ en_y4_l8_readq[] = { + {"What can people borrow besides books?", {"Cars", "Tools", "Flats", "Tickets to Berlin"}, 4, 1}, + {"Why did the idea start?", {"People were buying things they would use once", "The reading room was too popular", "Teenagers hated books", "The tools were illegal"}, 4, 0}, + {"Who has visited the workshop more, according to her?", {"Critics", "Teenagers, more than the reading room", "Other branches", "Nobody"}, 4, 1}, + {"What must users not do?", {"Return the object", "Clean it", "Lend it to someone else", "Visit the library"}, 4, 2}, +}; +static const char *en_y4_l8_readq_hints[] = { + "Hammers, sewing machines, a cake tin.", + "buying things they would use once.", + "more teenagers than the reading room.", + "must not lend it to anyone else.", +}; + +static const char *en_y4_l8_listen = "Welcome to the night-train podcast. The 22:40 to Berlin has been delayed by forty minutes because a tree is on the line near Ústí. Passengers who already have a seat can wait in carriage four, where tea is being served. If you have not checked in, please do it on your phone. We would be grateful if you kept the corridor clear. The delay might be shorter. We will know more at ten."; + +static const ChoiceQ en_y4_l8_listenq[] = { + {"How long is the delay?", {"14 minutes", "40 minutes", "4 hours", "22 minutes"}, 4, 1}, + {"Why?", {"Snow", "A tree on the line", "A missing driver", "A festival"}, 4, 1}, + {"Where can passengers with a seat wait?", {"Carriage four", "The airport", "Ústí station only", "The corridor"}, 4, 0}, + {"What should people without a check-in do?", {"Buy tea", "Check in on their phone", "Leave the train", "Call the mayor"}, 4, 1}, +}; +static const char *en_y4_l8_listenq_hints[] = { + "forty minutes.", + "a tree is on the line.", + "carriage four.", + "do it on your phone.", +}; + +static const char *en_y4_l8_write[] = { + "Write at least 45 words to a friend. Invite them to borrow something from a library of things. Use because and if.", + "Hi Adam, the library now lends tools, and I think you would like it because you always need a drill for one afternoon. If you come on Saturday, I will show you how it works. You have to bring your card and return everything clean. We could borrow a cake tin and bake something after. Let me know if you are free.", + "because|if", + "45", +}; + +static const ChoiceQ en_y4_l8_quiz[] = { + {"Tea ___ in carriage four.", {"serves", "is being served", "is serving", "has serve"}, 4, 1}, + {"We would be grateful if you ___ the corridor clear.", {"keep", "kept", "keeping", "to keep"}, 4, 1}, + {"She said the workshop ___ by more teenagers.", {"visited", "had been visited", "has visit", "is visiting"}, 4, 1}, + {"Users ___ return the tools clean.", {"have to", "must to", "don't must", "are have"}, 4, 0}, +}; +static const char *en_y4_l8_quiz_hints[] = { + "Průběhový trpný rod.", + "Would be grateful if + minulý tvar.", + "Had been visited.", + "Have to.", +}; + +static const EnLesson en_lessons[EN_YEARS][EN_N] = { + { + { + .slides = en_y1_l1_slides, + .n_slides = (int)(sizeof en_y1_l1_slides / sizeof en_y1_l1_slides[0]), + .gaps = en_y1_l1_gaps, + .n_gaps = (int)(sizeof en_y1_l1_gaps / sizeof en_y1_l1_gaps[0]), + .quiz = en_y1_l1_quiz, + .quiz_hints = en_y1_l1_quiz_hints, + .n_quiz = (int)(sizeof en_y1_l1_quiz / sizeof en_y1_l1_quiz[0]), + }, + { + .slides = en_y1_l2_slides, + .n_slides = (int)(sizeof en_y1_l2_slides / sizeof en_y1_l2_slides[0]), + .reading = "My school day starts early. I get up at half past six, have breakfast and catch the tram. Lessons begin at eight. On Monday we have maths, English and biology. I like biology because we do experiments, but I don't like maths. We have lunch at half past twelve in the canteen. After school I do my homework or meet friends. I never go to bed after ten on a school night.", + .readq = en_y1_l2_readq, + .read_hints = en_y1_l2_readq_hints, + .n_read = (int)(sizeof en_y1_l2_readq / sizeof en_y1_l2_readq[0]), + .gaps = en_y1_l2_gaps, + .n_gaps = (int)(sizeof en_y1_l2_gaps / sizeof en_y1_l2_gaps[0]), + }, + { + .slides = en_y1_l3_slides, + .n_slides = (int)(sizeof en_y1_l3_slides / sizeof en_y1_l3_slides[0]), + .listening = "Hi Tom, it's Anna. Are you free on Saturday? I'm meeting Eva at the station at ten. We're going to the market first, and then we're having lunch in that new café by the river. In the afternoon I'm playing volleyball with my class in the park. Do you want to come to the café? Call me back tonight.", + .listenq = en_y1_l3_listenq, + .listen_hints = en_y1_l3_listenq_hints, + .n_listen = (int)(sizeof en_y1_l3_listenq / sizeof en_y1_l3_listenq[0]), + .gaps = en_y1_l3_gaps, + .n_gaps = (int)(sizeof en_y1_l3_gaps / sizeof en_y1_l3_gaps[0]), + }, + { + .slides = en_y1_l4_slides, + .n_slides = (int)(sizeof en_y1_l4_slides / sizeof en_y1_l4_slides[0]), + .write_prompt = "Describe your room in at least 30 words. Say what is in it and where things are. Use on and a piece of furniture such as a bed, and mention a window.", + .write_model = "My room is small but bright. There is a bed next to the window and a wooden desk under a shelf. My books are on the desk and my school bag is under the chair. There are two posters on the wall. I like the room because it is quiet in the evening.", + .write_keys = "bed|window|on", + .write_min = 30, + .quiz = en_y1_l4_quiz, + .quiz_hints = en_y1_l4_quiz_hints, + .n_quiz = (int)(sizeof en_y1_l4_quiz / sizeof en_y1_l4_quiz[0]), + }, + { + .slides = en_y1_l5_slides, + .n_slides = (int)(sizeof en_y1_l5_slides / sizeof en_y1_l5_slides[0]), + .reading = "On Saturday morning the market by the river is busy. A woman asks, \"How much is this cheese?\" The seller says, \"It's four euros a kilo.\" Then she asks, \"Have you got any bread?\" He answers, \"Sorry, we haven't got any bread today, but we've got some fresh rolls.\" \"How many rolls do you want?\" \"Six, please, and a little milk.\" She also buys a few apples. \"Anything else?\" \"No, that's all.\"", + .readq = en_y1_l5_readq, + .read_hints = en_y1_l5_readq_hints, + .n_read = (int)(sizeof en_y1_l5_readq / sizeof en_y1_l5_readq[0]), + .gaps = en_y1_l5_gaps, + .n_gaps = (int)(sizeof en_y1_l5_gaps / sizeof en_y1_l5_gaps[0]), + }, + { + .slides = en_y1_l6_slides, + .n_slides = (int)(sizeof en_y1_l6_slides / sizeof en_y1_l6_slides[0]), + .listening = "Last July we went to Croatia. We travelled by train and then by bus because the flight was too expensive. The weather was hot every day. I swam in the morning and my brother tried surfing. He fell off the board a lot, but he loved it. One day we visited a small island. We didn't stay in a hotel. We stayed with our uncle near the beach. I took hundreds of photos.", + .listenq = en_y1_l6_listenq, + .listen_hints = en_y1_l6_listenq_hints, + .n_listen = (int)(sizeof en_y1_l6_listenq / sizeof en_y1_l6_listenq[0]), + .quiz = en_y1_l6_quiz, + .quiz_hints = en_y1_l6_quiz_hints, + .n_quiz = (int)(sizeof en_y1_l6_quiz / sizeof en_y1_l6_quiz[0]), + }, + { + .slides = en_y1_l7_slides, + .n_slides = (int)(sizeof en_y1_l7_slides / sizeof en_y1_l7_slides[0]), + .gaps = en_y1_l7_gaps, + .n_gaps = (int)(sizeof en_y1_l7_gaps / sizeof en_y1_l7_gaps[0]), + .write_prompt = "Write a short message to a friend about next weekend. Use at least 25 words. Say what you are going to do, mention the weekend, and give a reason with because.", + .write_model = "Hi Lena, next weekend I am going to visit my cousins in Pilsen. We are going to the cinema on Saturday because a new film is on. On Sunday we are going to cook lunch together. Do you want to come with me? Write back soon.", + .write_keys = "going|weekend|because", + .write_min = 25, + }, + { + .slides = en_y1_l8_slides, + .n_slides = (int)(sizeof en_y1_l8_slides / sizeof en_y1_l8_slides[0]), + .reading = "Klara lives in a flat with her parents and a small dog. On weekdays she walks to school because it is only ten minutes away. She loves art. Last Friday her class visited a gallery and she bought a poster. This Saturday she is meeting her cousin at the station. They are going to a concert in the evening.", + .readq = en_y1_l8_readq, + .read_hints = en_y1_l8_readq_hints, + .n_read = (int)(sizeof en_y1_l8_readq / sizeof en_y1_l8_readq[0]), + .listening = "Hi, it's Marek. I missed you after school. I'm at the sports centre. The match starts at four, not at three, so don't hurry. Can you bring my blue jacket? It's on the chair in the kitchen. See you there.", + .listenq = en_y1_l8_listenq, + .listen_hints = en_y1_l8_listenq_hints, + .n_listen = (int)(sizeof en_y1_l8_listenq / sizeof en_y1_l8_listenq[0]), + .write_prompt = "Write at least 30 words about your family. Use is or are, and the words family and because.", + .write_model = "My family is not very big. I live with my parents and my younger sister. She is twelve and she is funny. We get on well because we both like films. My dad is a driver and my mum is a nurse. I am happy at home.", + .write_keys = "family|because", + .write_min = 30, + .quiz = en_y1_l8_quiz, + .quiz_hints = en_y1_l8_quiz_hints, + .n_quiz = (int)(sizeof en_y1_l8_quiz / sizeof en_y1_l8_quiz[0]), + } + }, + { + { + .slides = en_y2_l1_slides, + .n_slides = (int)(sizeof en_y2_l1_slides / sizeof en_y2_l1_slides[0]), + .gaps = en_y2_l1_gaps, + .n_gaps = (int)(sizeof en_y2_l1_gaps / sizeof en_y2_l1_gaps[0]), + .quiz = en_y2_l1_quiz, + .quiz_hints = en_y2_l1_quiz_hints, + .n_quiz = (int)(sizeof en_y2_l1_quiz / sizeof en_y2_l1_quiz[0]), + }, + { + .slides = en_y2_l2_slides, + .n_slides = (int)(sizeof en_y2_l2_slides / sizeof en_y2_l2_slides[0]), + .reading = "The school clinic is open from seven to three. If you feel ill, you should tell your teacher first. You have to bring your insurance card. You must not come to school with a high temperature. Students with a cough should wear a mask in the waiting room. You don't have to make an appointment for a small cut, but you should come before noon. The nurse can give you a painkiller. She can't give you antibiotics.", + .readq = en_y2_l2_readq, + .read_hints = en_y2_l2_readq_hints, + .n_read = (int)(sizeof en_y2_l2_readq / sizeof en_y2_l2_readq[0]), + .quiz = en_y2_l2_quiz, + .quiz_hints = en_y2_l2_quiz_hints, + .n_quiz = (int)(sizeof en_y2_l2_quiz / sizeof en_y2_l2_quiz[0]), + }, + { + .slides = en_y2_l3_slides, + .n_slides = (int)(sizeof en_y2_l3_slides / sizeof en_y2_l3_slides[0]), + .listening = "Manager: Have you ever worked in a shop? Tereza: Not in a shop, but I have helped in my uncle's bakery since May. Manager: What do you do there? Tereza: I have served customers and I have learned to use the till. I haven't baked bread yet. Manager: Can you start next Saturday? Tereza: Yes. I have already asked my uncle, and he says it's fine. I can't work on Sunday mornings because I have a match.", + .listenq = en_y2_l3_listenq, + .listen_hints = en_y2_l3_listenq_hints, + .n_listen = (int)(sizeof en_y2_l3_listenq / sizeof en_y2_l3_listenq[0]), + .gaps = en_y2_l3_gaps, + .n_gaps = (int)(sizeof en_y2_l3_gaps / sizeof en_y2_l3_gaps[0]), + }, + { + .slides = en_y2_l4_slides, + .n_slides = (int)(sizeof en_y2_l4_slides / sizeof en_y2_l4_slides[0]), + .write_prompt = "Write a short review of a film or a book in at least 40 words. Use who once and which once, and say who you would recommend it to.", + .write_model = "The Lighthouse Keeper is a quiet film which follows a woman on a small island. The actor who plays her hardly speaks, but her face tells the story. The sea, which is almost a character, is beautiful and dangerous. I would recommend it to anyone who likes slow films. The ending is sad but honest.", + .write_keys = "who|which|recommend", + .write_min = 40, + .quiz = en_y2_l4_quiz, + .quiz_hints = en_y2_l4_quiz_hints, + .n_quiz = (int)(sizeof en_y2_l4_quiz / sizeof en_y2_l4_quiz[0]), + }, + { + .slides = en_y2_l5_slides, + .n_slides = (int)(sizeof en_y2_l5_slides / sizeof en_y2_l5_slides[0]), + .reading = "We wanted to cycle to the lake, but the morning was too foggy to start. By ten the sun was out and the path was dry enough. There were too many cyclists near the dam, so we took a quieter road. We didn't have enough water, which was a mistake. At the lake a guard said, \"Look at that cloud. It's going to pour. I think the storm will last an hour.\" We were not fast enough. We got soaked.", + .readq = en_y2_l5_readq, + .read_hints = en_y2_l5_readq_hints, + .n_read = (int)(sizeof en_y2_l5_readq / sizeof en_y2_l5_readq[0]), + .gaps = en_y2_l5_gaps, + .n_gaps = (int)(sizeof en_y2_l5_gaps / sizeof en_y2_l5_gaps[0]), + }, + { + .slides = en_y2_l6_slides, + .n_slides = (int)(sizeof en_y2_l6_slides / sizeof en_y2_l6_slides[0]), + .listening = "This is Nora at the river festival. The gates have just opened and hundreds of people have already come in. I have never seen so many lanterns. Last night the main band played for two hours, but tonight's singer hasn't arrived yet. A local baker has made a cake in the shape of a violin. I tried a piece yesterday. It was delicious. Have you ever been here? I came for the first time in 2022, and I have come back every summer since then.", + .listenq = en_y2_l6_listenq, + .listen_hints = en_y2_l6_listenq_hints, + .n_listen = (int)(sizeof en_y2_l6_listenq / sizeof en_y2_l6_listenq[0]), + .quiz = en_y2_l6_quiz, + .quiz_hints = en_y2_l6_quiz_hints, + .n_quiz = (int)(sizeof en_y2_l6_quiz / sizeof en_y2_l6_quiz[0]), + }, + { + .slides = en_y2_l7_slides, + .n_slides = (int)(sizeof en_y2_l7_slides / sizeof en_y2_l7_slides[0]), + .gaps = en_y2_l7_gaps, + .n_gaps = (int)(sizeof en_y2_l7_gaps / sizeof en_y2_l7_gaps[0]), + .write_prompt = "Write at least 35 words. Include one real plan with if and will, and one unreal dream with would. Use the word if twice.", + .write_model = "If I finish my project tonight, I will go climbing with Eva on Saturday. We will take the early bus if the weather is fine. If I had more free time, I would learn the guitar. I would also travel by night train if tickets were cheaper. If is a small word with two very different lives.", + .write_keys = "if|will|would", + .write_min = 35, + }, + { + .slides = en_y2_l8_slides, + .n_slides = (int)(sizeof en_y2_l8_slides / sizeof en_y2_l8_slides[0]), + .reading = "The night bus to Brno is cheaper than the train, but it is also slower. Last Friday I was sleeping when the driver stopped at a dark petrol station. A woman who had missed her connection asked if she could sit next to me. She has travelled this route since March because her new job starts at six. If the bus is late, she has to run. She says she would move closer if flats were cheaper.", + .readq = en_y2_l8_readq, + .read_hints = en_y2_l8_readq_hints, + .n_read = (int)(sizeof en_y2_l8_readq / sizeof en_y2_l8_readq[0]), + .listening = "Doctor: How long have you had this cough? Adam: Since Sunday. I have also had a sore throat for two days. Doctor: You should rest and drink water. You don't have to stay in bed all day, but you mustn't play football this week. If you still feel ill on Friday, come back.", + .listenq = en_y2_l8_listenq, + .listen_hints = en_y2_l8_listenq_hints, + .n_listen = (int)(sizeof en_y2_l8_listenq / sizeof en_y2_l8_listenq[0]), + .write_prompt = "Write at least 35 words about a trip or a job you would like. Use would and because.", + .write_model = "I would like to work in a small bookshop by the station because I love quiet mornings. I would talk to customers and I would also read in the breaks. If I had the job, I would cycle there. I have never worked in a shop, but I have helped at school events.", + .write_keys = "would|because", + .write_min = 35, + .quiz = en_y2_l8_quiz, + .quiz_hints = en_y2_l8_quiz_hints, + .n_quiz = (int)(sizeof en_y2_l8_quiz / sizeof en_y2_l8_quiz[0]), + } + }, + { + { + .slides = en_y3_l1_slides, + .n_slides = (int)(sizeof en_y3_l1_slides / sizeof en_y3_l1_slides[0]), + .gaps = en_y3_l1_gaps, + .n_gaps = (int)(sizeof en_y3_l1_gaps / sizeof en_y3_l1_gaps[0]), + .quiz = en_y3_l1_quiz, + .quiz_hints = en_y3_l1_quiz_hints, + .n_quiz = (int)(sizeof en_y3_l1_quiz / sizeof en_y3_l1_quiz[0]), + }, + { + .slides = en_y3_l2_slides, + .n_slides = (int)(sizeof en_y3_l2_slides / sizeof en_y3_l2_slides[0]), + .reading = "Our head teacher told the local paper that the school was changing. She said that students would start an hour later next term. One pupil told the reporter that he was pleased because he hated early buses. A parent said that she was worried. She said her daughter had joined three clubs and had very little time. The cook said that lunch would still be at noon. He added that the menu was going to include more vegetables.", + .readq = en_y3_l2_readq, + .read_hints = en_y3_l2_readq_hints, + .n_read = (int)(sizeof en_y3_l2_readq / sizeof en_y3_l2_readq[0]), + .quiz = en_y3_l2_quiz, + .quiz_hints = en_y3_l2_quiz_hints, + .n_quiz = (int)(sizeof en_y3_l2_quiz / sizeof en_y3_l2_quiz[0]), + }, + { + .slides = en_y3_l3_slides, + .n_slides = (int)(sizeof en_y3_l3_slides / sizeof en_y3_l3_slides[0]), + .listening = "Last month our class recorded a podcast called The Last Beech. A ranger said that the trees were thirsty because the summers had become hotter. She told us that volunteers had planted two thousand young trees, but she added that many would die unless it rained. If the town banned cars from the forest road, the air would be cleaner. If people keep using it as a shortcut, the soil will wash away.", + .listenq = en_y3_l3_listenq, + .listen_hints = en_y3_l3_listenq_hints, + .n_listen = (int)(sizeof en_y3_l3_listenq / sizeof en_y3_l3_listenq[0]), + .gaps = en_y3_l3_gaps, + .n_gaps = (int)(sizeof en_y3_l3_gaps / sizeof en_y3_l3_gaps[0]), + }, + { + .slides = en_y3_l4_slides, + .n_slides = (int)(sizeof en_y3_l4_slides / sizeof en_y3_l4_slides[0]), + .write_prompt = "Write a for-and-against paragraph of at least 50 words about school uniforms. Use however and because, and finish with your opinion using believe.", + .write_model = "School uniforms are a common argument. They are practical because students do not have to decide what to wear, and families spend less money. However, a uniform hides personality and can feel childish at eighteen. Although it makes mornings faster, it does not make lessons better. I believe schools should keep a simple dress code, but they should not force one jacket on everyone.", + .write_keys = "however|because|believe", + .write_min = 50, + .quiz = en_y3_l4_quiz, + .quiz_hints = en_y3_l4_quiz_hints, + .n_quiz = (int)(sizeof en_y3_l4_quiz / sizeof en_y3_l4_quiz[0]), + }, + { + .slides = en_y3_l5_slides, + .n_slides = (int)(sizeof en_y3_l5_slides / sizeof en_y3_l5_slides[0]), + .reading = "Someone left a strange machine in the physics lab. It was warm, so it must have been switched on recently. The only key can't have been used by the caretaker, because he is in hospital. Two students, Nora and Adam, know the code. Nora might have come back after the club, but her train ticket shows she was in Pardubice. Adam's notebook is on the bench and the handwriting matches the label. He must have built it.", + .readq = en_y3_l5_readq, + .read_hints = en_y3_l5_readq_hints, + .n_read = (int)(sizeof en_y3_l5_readq / sizeof en_y3_l5_readq[0]), + .quiz = en_y3_l5_quiz, + .quiz_hints = en_y3_l5_quiz_hints, + .n_quiz = (int)(sizeof en_y3_l5_quiz / sizeof en_y3_l5_quiz[0]), + }, + { + .slides = en_y3_l6_slides, + .n_slides = (int)(sizeof en_y3_l6_slides / sizeof en_y3_l6_slides[0]), + .listening = "Reporter: Mayor, is the old bridge safe? Mayor: I can't promise that. An engineer told me that two cables were weak. Reporter: How long will the repairs take? Mayor: She asked the firm the same question yesterday. They said they didn't know yet. Reporter: Did anyone complain? Mayor: A shopkeeper asked me why the bus stop had moved. I asked her if she could wait until Friday. Reporter: Will you close the bridge? Mayor: We might. I have asked the police whether they can send officers tonight.", + .listenq = en_y3_l6_listenq, + .listen_hints = en_y3_l6_listenq_hints, + .n_listen = (int)(sizeof en_y3_l6_listenq / sizeof en_y3_l6_listenq[0]), + .quiz = en_y3_l6_quiz, + .quiz_hints = en_y3_l6_quiz_hints, + .n_quiz = (int)(sizeof en_y3_l6_quiz / sizeof en_y3_l6_quiz[0]), + }, + { + .slides = en_y3_l7_slides, + .n_slides = (int)(sizeof en_y3_l7_slides / sizeof en_y3_l7_slides[0]), + .gaps = en_y3_l7_gaps, + .n_gaps = (int)(sizeof en_y3_l7_gaps / sizeof en_y3_l7_gaps[0]), + .quiz = en_y3_l7_quiz, + .quiz_hints = en_y3_l7_quiz_hints, + .n_quiz = (int)(sizeof en_y3_l7_quiz / sizeof en_y3_l7_quiz[0]), + }, + { + .slides = en_y3_l8_slides, + .n_slides = (int)(sizeof en_y3_l8_slides / sizeof en_y3_l8_slides[0]), + .reading = "A repair café opened in a former bakery. Broken radios are mended there on Saturdays. The founder said that people would rather repair than buy if they were shown how. She told a reporter that twenty kettles had been saved in March. Visitors have to bring the broken object, but they don't have to pay. If the café closed, a lot of working parts would be thrown away.", + .readq = en_y3_l8_readq, + .read_hints = en_y3_l8_readq_hints, + .n_read = (int)(sizeof en_y3_l8_readq / sizeof en_y3_l8_readq[0]), + .listening = "Editor: Did the mayor answer you? Journalist: I asked him whether the bridge was safe. He said he didn't know. I also asked how long the repairs would take. He told me that the firm hadn't decided. A resident asked me why the paper hadn't printed a map. I said we would print one tomorrow.", + .listenq = en_y3_l8_listenq, + .listen_hints = en_y3_l8_listenq_hints, + .n_listen = (int)(sizeof en_y3_l8_listenq / sizeof en_y3_l8_listenq[0]), + .write_prompt = "In at least 40 words, give one reason for repairing things and one objection. Use because and however.", + .write_model = "Repairing things matters because a kettle can work for years after a small fix. However, some people do not have the tools or the time, so a café where volunteers help is a fair idea. I believe every town should have one quiet room for broken objects. It is slower than shopping, but it is kinder.", + .write_keys = "because|however", + .write_min = 40, + .quiz = en_y3_l8_quiz, + .quiz_hints = en_y3_l8_quiz_hints, + .n_quiz = (int)(sizeof en_y3_l8_quiz / sizeof en_y3_l8_quiz[0]), + } + }, + { + { + .slides = en_y4_l1_slides, + .n_slides = (int)(sizeof en_y4_l1_slides / sizeof en_y4_l1_slides[0]), + .gaps = en_y4_l1_gaps, + .n_gaps = (int)(sizeof en_y4_l1_gaps / sizeof en_y4_l1_gaps[0]), + .quiz = en_y4_l1_quiz, + .quiz_hints = en_y4_l1_quiz_hints, + .n_quiz = (int)(sizeof en_y4_l1_quiz / sizeof en_y4_l1_quiz[0]), + }, + { + .slides = en_y4_l2_slides, + .n_slides = (int)(sizeof en_y4_l2_slides / sizeof en_y4_l2_slides[0]), + .write_prompt = "Write a formal email of at least 55 words to a railway company. Complain about a delay. Start with Dear, use would, and end with sincerely or faithfully.", + .write_model = "Dear Sir or Madam, I am writing to complain about the night train to Berlin on 3 May. It arrived ninety minutes late, so I missed a morning interview. I would be grateful if you could explain the delay and offer a refund. I have attached my ticket. Please reply to this address. Yours faithfully, Tereza Malá.", + .write_keys = "dear|would|faithfully", + .write_min = 55, + .quiz = en_y4_l2_quiz, + .quiz_hints = en_y4_l2_quiz_hints, + .n_quiz = (int)(sizeof en_y4_l2_quiz / sizeof en_y4_l2_quiz[0]), + }, + { + .slides = en_y4_l3_slides, + .n_slides = (int)(sizeof en_y4_l3_slides / sizeof en_y4_l3_slides[0]), + .reading = "When the night train from Prague to Berlin returned last spring, stations filled with people who prefer a bed to a boarding gate. A single ticket is often more expensive than a flight, which surprises anyone who thinks trains are always the cheap choice. Still, passengers say they arrive calmer. They can read, talk and avoid the bright shops of an airport. Critics point out that a delay of two hours is common and that the cabins are small. The railway has promised that new carriages will be added if demand stays high. A student I spoke to had taken the train three times. She said she would fly only if she missed the evening departure. For her, the journey is part of the trip, not a problem to be deleted.", + .readq = en_y4_l3_readq, + .read_hints = en_y4_l3_readq_hints, + .n_read = (int)(sizeof en_y4_l3_readq / sizeof en_y4_l3_readq[0]), + }, + { + .slides = en_y4_l4_slides, + .n_slides = (int)(sizeof en_y4_l4_slides / sizeof en_y4_l4_slides[0]), + .listening = "Interviewer: Why did you apply to the summer camp in Wales? Hana: I have worked with children for two years, and I wanted a job where English is the only language. Interviewer: What would you do if a child refused to join an activity? Hana: I wouldn't force them. I would ask why, and I might offer a quieter task. Interviewer: Have you ever dealt with an emergency? Hana: Yes. Last August a boy cut his foot. I cleaned the cut and called the nurse. I didn't give him any medicine. Interviewer: The camp is rainy. Is that a problem? Hana: Not really. I have already bought boots. I look forward to the hills, although I know the work will be tiring.", + .listenq = en_y4_l4_listenq, + .listen_hints = en_y4_l4_listenq_hints, + .n_listen = (int)(sizeof en_y4_l4_listenq / sizeof en_y4_l4_listenq[0]), + }, + { + .slides = en_y4_l5_slides, + .n_slides = (int)(sizeof en_y4_l5_slides / sizeof en_y4_l5_slides[0]), + .write_prompt = "Write an opinion of at least 60 words: should students have a gap year before university? Use believe, although and in conclusion.", + .write_model = "I believe a gap year can help, but only when it has a plan. Although some students waste the time, others work, travel or look after a relative and return calmer. A year in a job also shows whether a degree is really necessary. On the other hand, a long break can make it harder to study again. In conclusion, a gap year is useful if the person can explain what they will do with it.", + .write_keys = "believe|although|conclusion", + .write_min = 60, + .quiz = en_y4_l5_quiz, + .quiz_hints = en_y4_l5_quiz_hints, + .n_quiz = (int)(sizeof en_y4_l5_quiz / sizeof en_y4_l5_quiz[0]), + }, + { + .slides = en_y4_l6_slides, + .n_slides = (int)(sizeof en_y4_l6_slides / sizeof en_y4_l6_slides[0]), + .gaps = en_y4_l6_gaps, + .n_gaps = (int)(sizeof en_y4_l6_gaps / sizeof en_y4_l6_gaps[0]), + .quiz = en_y4_l6_quiz, + .quiz_hints = en_y4_l6_quiz_hints, + .n_quiz = (int)(sizeof en_y4_l6_quiz / sizeof en_y4_l6_quiz[0]), + }, + { + .slides = en_y4_l7_slides, + .n_slides = (int)(sizeof en_y4_l7_slides / sizeof en_y4_l7_slides[0]), + .gaps = en_y4_l7_gaps, + .n_gaps = (int)(sizeof en_y4_l7_gaps / sizeof en_y4_l7_gaps[0]), + .quiz = en_y4_l7_quiz, + .quiz_hints = en_y4_l7_quiz_hints, + .n_quiz = (int)(sizeof en_y4_l7_quiz / sizeof en_y4_l7_quiz[0]), + }, + { + .slides = en_y4_l8_slides, + .n_slides = (int)(sizeof en_y4_l8_slides / sizeof en_y4_l8_slides[0]), + .reading = "The town library now lends tools as well as books. Hammers, sewing machines and a cake tin can be borrowed for a week. The idea came from a librarian who was tired of people buying things they would use once. She said the workshop had been visited by more teenagers than the reading room. Critics argue that a library should stay quiet. She replies that a library should be useful. If the experiment works, other branches will copy it. Users have to return each object clean, and they must not lend it to anyone else.", + .readq = en_y4_l8_readq, + .read_hints = en_y4_l8_readq_hints, + .n_read = (int)(sizeof en_y4_l8_readq / sizeof en_y4_l8_readq[0]), + .listening = "Welcome to the night-train podcast. The 22:40 to Berlin has been delayed by forty minutes because a tree is on the line near Ústí. Passengers who already have a seat can wait in carriage four, where tea is being served. If you have not checked in, please do it on your phone. We would be grateful if you kept the corridor clear. The delay might be shorter. We will know more at ten.", + .listenq = en_y4_l8_listenq, + .listen_hints = en_y4_l8_listenq_hints, + .n_listen = (int)(sizeof en_y4_l8_listenq / sizeof en_y4_l8_listenq[0]), + .write_prompt = "Write at least 45 words to a friend. Invite them to borrow something from a library of things. Use because and if.", + .write_model = "Hi Adam, the library now lends tools, and I think you would like it because you always need a drill for one afternoon. If you come on Saturday, I will show you how it works. You have to bring your card and return everything clean. We could borrow a cake tin and bake something after. Let me know if you are free.", + .write_keys = "because|if", + .write_min = 45, + .quiz = en_y4_l8_quiz, + .quiz_hints = en_y4_l8_quiz_hints, + .n_quiz = (int)(sizeof en_y4_l8_quiz / sizeof en_y4_l8_quiz[0]), + } + }, +}; + diff --git a/src/graduately.h b/src/graduately.h index 27d5ba0..c74043f 100644 --- a/src/graduately.h +++ b/src/graduately.h @@ -20,6 +20,7 @@ #define HW_SUBJ 2 /* index of "Technické vybavení" */ #define CZ_SUBJ 3 /* index of "Český jazyk a literatura" */ #define ON_SUBJ 4 /* index of "Občanská nauka" */ +#define EN_SUBJ 5 /* index of "English" */ #define MATH_SUBJ 6 /* index of "Matematika" */ #define PHY_SUBJ 7 /* index of "Fyzika" */ #define SCI_SUBJ 8 /* index of "Základy Přírodopisných věd" */ @@ -56,6 +57,7 @@ extern char *app_progress_on; extern char *app_progress_on2; extern char *app_progress_on3; extern char *app_progress_on4; +extern char *app_progress_en; extern char *app_progress_mluvnice; extern char *app_progress_cetba; extern char *app_progress_lit; @@ -84,6 +86,7 @@ extern char *app_progress_mat4; #define PROGRESS_ON2 app_progress_on2 #define PROGRESS_ON3 app_progress_on3 #define PROGRESS_ON4 app_progress_on4 +#define PROGRESS_EN app_progress_en #define NODE_SIZE 88.0 #define PATH_SPAC 240.0 #define ROAD_MX 150.0 /* horizontal canvas margin */ @@ -903,6 +906,11 @@ void on_lessons_apply_lang(void); GtkWidget *build_onyears_page(void); GtkWidget *build_onmap_page(void); void add_on_pages(GtkStack *stack); +void en_load_progress(void); +void progress_for_en(ProgressSum *out); +void en_rail_theme_reset(void); +GtkWidget *build_enyears_page(void); +void add_en_pages(GtkStack *stack); void on2_load_progress(void); void on2_save_progress(void); void mark_on2_done(int lesson_id); @@ -1117,6 +1125,8 @@ void draw_physics_icon(GtkDrawingArea *area, cairo_t *cr, int width, int height, gpointer data); void draw_czech_flag(GtkDrawingArea *area, cairo_t *cr, int width, int height, gpointer data); +void draw_uk_flag(GtkDrawingArea *area, cairo_t *cr, + int width, int height, gpointer data); void draw_book_icon(GtkDrawingArea *area, cairo_t *cr, int width, int height, gpointer data); void draw_book_badge_icon(GtkDrawingArea *area, cairo_t *cr, diff --git a/src/i18n.c b/src/i18n.c index 746b0ad..e88d2b7 100644 --- a/src/i18n.c +++ b/src/i18n.c @@ -271,7 +271,7 @@ const TrEntry tr_ui[] = { {"Český jazyk a literatura", "Český jazyk a literatura", "Czech Language and Literature"}, {"Občanská nauka", "Občanská nauka", "Civics"}, - {"English", "English", "English"}, + {"English", "Angličtina", "English"}, {"Deutsch", "Deutsch", "Deutsch"}, {"Matematika", "Matematika", "Mathematics"}, {"Fyzika", "Fyzika", "Physics"}, @@ -1619,6 +1619,7 @@ const TrEntry tr_ui[] = { {"lit_fuks_plot_title", "Sestavte děj", "Order the plot"}, {"lit_fuks_plot_sub", "Přetáhněte části příběhu do správného pořadí.", "Drag the story parts into the correct order."}, +#include "english_i18n.inc" #include "math_i18n.inc" {NULL, NULL, NULL} }; @@ -2601,6 +2602,7 @@ void on_lessons_apply_lang(void); void on2_lessons_apply_lang(void); void on3_lessons_apply_lang(void); void on4_lessons_apply_lang(void); +void en_lessons_apply_lang(void); void lit_lessons_apply_lang(void); void sci_lessons_apply_lang(void); void fyz_lessons_apply_lang(void); @@ -2623,6 +2625,7 @@ void apply_language(void) { on2_lessons_apply_lang(); on3_lessons_apply_lang(); on4_lessons_apply_lang(); + en_lessons_apply_lang(); lit_lessons_apply_lang(); sci_lessons_apply_lang(); fyz_lessons_apply_lang(); diff --git a/src/main.c b/src/main.c index 4c42c62..22fdc6d 100644 --- a/src/main.c +++ b/src/main.c @@ -416,6 +416,8 @@ static gboolean run_smoke_test(gpointer data) { "on2unit1", "on2ex1", "on2unit16", "on2ex16", "on3unit1", "on3ex1", "on3unit16", "on3ex16", "on4unit1", "on4ex1", "on4unit16", "on4ex16", + "enyears", "en1map", "en4map", + "en1unit1", "en1ex1", "en4unit8", "en4ex8", "unit1", "unit2", "unit3", "u1e1", "u1e13", "u2e1", "u2e19", "u3e1", "u3e15", "u1vocab", "u2vocab", "u3vocab", @@ -534,6 +536,8 @@ void activate(GtkApplication *app, gpointer user_data) { gtk_stack_add_named(main_stack, build_on3map_page(), "on3map"); add_on3_pages(main_stack); gtk_stack_add_named(main_stack, build_on4map_page(), "on4map"); + gtk_stack_add_named(main_stack, build_enyears_page(), "enyears"); + add_en_pages(main_stack); add_on4_pages(main_stack); gtk_stack_add_named(main_stack, build_matyears_page(), "matyears"); gtk_stack_add_named(main_stack, build_mat0map_page(), "mat0map"); diff --git a/src/progress.c b/src/progress.c index f84c39d..c7ae73d 100644 --- a/src/progress.c +++ b/src/progress.c @@ -58,6 +58,7 @@ void load_progress(void) { on2_load_progress(); on3_load_progress(); on4_load_progress(); + en_load_progress(); lit_load_progress(); sci_load_progress(); fyz_load_progress(); diff --git a/src/search.c b/src/search.c index 6f5f9d3..faedd31 100644 --- a/src/search.c +++ b/src/search.c @@ -23,7 +23,8 @@ static GPtrArray *search_items; static gboolean subject_open(int i) { return i == 0 || i == NET_SUBJ || i == HW_SUBJ || i == CZ_SUBJ - || i == ON_SUBJ || i == MATH_SUBJ || i == PHY_SUBJ || i == SCI_SUBJ; + || i == ON_SUBJ || i == EN_SUBJ || i == MATH_SUBJ || i == PHY_SUBJ + || i == SCI_SUBJ; } static const char *subject_page(int i) { @@ -37,6 +38,8 @@ static const char *subject_page(int i) { return "czechmap"; if (i == ON_SUBJ) return "onyears"; + if (i == EN_SUBJ) + return "enyears"; if (i == MATH_SUBJ) return "matyears"; if (i == PHY_SUBJ) @@ -241,6 +244,21 @@ static void catalog_rebuild(void) { catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "kviz quiz"); } + catalog_add_key("en_year1", "search_lesson", "en1map", FALSE, 1, "anglictina english"); + catalog_add_key("en_year2", "search_lesson", "en2map", FALSE, 1, "anglictina english"); + catalog_add_key("en_year3", "search_lesson", "en3map", FALSE, 1, "anglictina english"); + catalog_add_key("en_year4", "search_lesson", "en4map", FALSE, 1, "anglictina english"); + for (int y = 1; y <= 4; y++) { + for (i = 1; i <= 8; i++) { + g_snprintf(key, sizeof(key), "en_y%d_u%d", y, i); + g_snprintf(page, sizeof(page), "en%dunit%d", y, i); + catalog_add_key(key, "search_lesson", page, FALSE, 2, "anglictina english"); + g_snprintf(page, sizeof(page), "en%dex%d", y, i); + catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, + "poslech cteni psani kviz"); + } + } + catalog_add_key("Český jazyk a literatura", "search_subject", "czechmap", FALSE, 0, "cestina czech"); catalog_add_key("Literatura", "search_lesson", "lityears", FALSE, 1, diff --git a/src/stats.c b/src/stats.c index 2996236..f013fe5 100644 --- a/src/stats.c +++ b/src/stats.c @@ -71,6 +71,8 @@ void refresh_stats_ui(void) { progress_for_hw(&all); else if (s == ON_SUBJ) progress_for_on(&all); + else if (s == EN_SUBJ) + progress_for_en(&all); else if (s == CZ_SUBJ) { progress_for_mluvnice(&all); progress_for_lit(&all); @@ -110,7 +112,8 @@ void refresh_stats_ui(void) { GtkWidget *bar = stats_ui.subj[s].bar; ProgressSum sp = {0}; gboolean has_content = (s == NET_SUBJ) || (s == HW_SUBJ) - || (s == ON_SUBJ) || (s == CZ_SUBJ) + || (s == ON_SUBJ) || (s == EN_SUBJ) + || (s == CZ_SUBJ) || (s == SCI_SUBJ) || (s == PHY_SUBJ) || (s == MATH_SUBJ) || sub_unit_count[s] > 0; @@ -123,6 +126,8 @@ void refresh_stats_ui(void) { progress_for_hw(&sp); else if (s == ON_SUBJ) progress_for_on(&sp); + else if (s == EN_SUBJ) + progress_for_en(&sp); else if (s == CZ_SUBJ) { progress_for_mluvnice(&sp); progress_for_lit(&sp); diff --git a/src/subjects.c b/src/subjects.c index 1b712fb..836351b 100644 --- a/src/subjects.c +++ b/src/subjects.c @@ -5,9 +5,8 @@ /* ------------------------------------------------------------------ */ /* Index 0 (Deutsch), NET_SUBJ (networks), HW_SUBJ (hardware), CZ_SUBJ - * (Czech), ON_SUBJ (civics), MATH_SUBJ (mathematics), PHY_SUBJ - * (physics) and SCI_SUBJ - * (natural sciences) are unlocked. */ + * (Czech), ON_SUBJ (civics), EN_SUBJ (English), MATH_SUBJ (mathematics), + * PHY_SUBJ (physics) and SCI_SUBJ (natural sciences) are unlocked. */ const char *sub_keys[NUM_SUBJECTS] = { "Deutsch", "Správa počítačových sítí", @@ -156,7 +155,8 @@ void draw_czech_flag(GtkDrawingArea *area, cairo_t *cr, static gboolean subject_is_open(int i) { return i == 0 || i == NET_SUBJ || i == HW_SUBJ || i == CZ_SUBJ - || i == ON_SUBJ || i == MATH_SUBJ || i == PHY_SUBJ || i == SCI_SUBJ; + || i == ON_SUBJ || i == EN_SUBJ || i == MATH_SUBJ || i == PHY_SUBJ + || i == SCI_SUBJ; } static GtkWidget *subject_icon(int i) { @@ -179,6 +179,9 @@ static GtkWidget *subject_icon(int i) { else if (i == ON_SUBJ) gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(area), draw_civics_icon, NULL, NULL); + else if (i == EN_SUBJ) + gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(area), + draw_uk_flag, NULL, NULL); else if (i == MATH_SUBJ) gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(area), draw_math_icon, NULL, NULL); @@ -204,6 +207,8 @@ static const char *subject_target(int i) { return "czechmap"; if (i == ON_SUBJ) return "onyears"; + if (i == EN_SUBJ) + return "enyears"; if (i == MATH_SUBJ) return "matyears"; if (i == PHY_SUBJ) diff --git a/src/theme.c b/src/theme.c index 3fe2061..d0d44d8 100644 --- a/src/theme.c +++ b/src/theme.c @@ -430,6 +430,7 @@ void apply_theme(void) { on2_rail_theme_reset(); on3_rail_theme_reset(); on4_rail_theme_reset(); + en_rail_theme_reset(); czech_rail_theme_reset(); lit_rail_theme_reset(); sci_rail_theme_reset(); diff --git a/src/util.c b/src/util.c index 387c909..3355187 100644 --- a/src/util.c +++ b/src/util.c @@ -24,6 +24,7 @@ char *app_progress_on; char *app_progress_on2; char *app_progress_on3; char *app_progress_on4; +char *app_progress_en; char *app_progress_mluvnice; char *app_progress_cetba; char *app_progress_lit; @@ -181,6 +182,7 @@ static void set_progress_paths(const char *write_root) { g_free(app_progress_on2); g_free(app_progress_on3); g_free(app_progress_on4); + g_free(app_progress_en); g_free(app_progress_mluvnice); g_free(app_progress_cetba); g_free(app_progress_lit); @@ -210,6 +212,7 @@ static void set_progress_paths(const char *write_root) { app_progress_on2 = g_build_filename(app_progress_dir, "on2.conf", NULL); app_progress_on3 = g_build_filename(app_progress_dir, "on3.conf", NULL); app_progress_on4 = g_build_filename(app_progress_dir, "on4.conf", NULL); + app_progress_en = g_build_filename(app_progress_dir, "english.conf", NULL); app_progress_mluvnice = g_build_filename(app_progress_dir, "mluvnice.conf", NULL); app_progress_cetba = g_build_filename(app_progress_dir, "cetba.conf", NULL); app_progress_lit = g_build_filename(app_progress_dir, "lit.conf", NULL);