From 438623d8575d3ea8f0e5d7602a49f453440d3a5c Mon Sep 17 00:00:00 2001 From: Stan Runge Date: Thu, 24 Sep 2026 17:34:56 +0000 Subject: [PATCH] add language learner project Dutch and Korean (explained in English) and English (explained in Dutch) from A1 to C1, with a Hangul level for Korean. Lessons with notes, words and sentences generate multiple-choice, typing, sentence building, cloze, listening and matching exercises. Endless challenges: a seeded daily challenge, sprint, survival, mixed practice, level tests, spaced-repetition reviews and generated drills (numbers, clock times, de/het, plurals, Dutch and Korean conjugation, particles, Hangul reading, irregular verbs). Progress is stored in localStorage with export/import. Co-Authored-By: Claude Opus 5.5 --- README.md | 26 + eslint.config.js | 2 +- src/lib/language-learner/challenges.test.ts | 33 + src/lib/language-learner/challenges.ts | 128 ++++ src/lib/language-learner/content.test.ts | 85 +++ src/lib/language-learner/course.ts | 111 ++++ src/lib/language-learner/courses/en/a1.ts | 567 +++++++++++++++++ src/lib/language-learner/courses/en/a2.ts | 419 +++++++++++++ src/lib/language-learner/courses/en/b1.ts | 401 ++++++++++++ src/lib/language-learner/courses/en/b2.ts | 347 ++++++++++ src/lib/language-learner/courses/en/c1.ts | 318 ++++++++++ src/lib/language-learner/courses/en/index.ts | 18 + src/lib/language-learner/courses/index.ts | 48 ++ src/lib/language-learner/courses/ko/a1.ts | 465 ++++++++++++++ src/lib/language-learner/courses/ko/a2.ts | 426 +++++++++++++ src/lib/language-learner/courses/ko/b1.ts | 404 ++++++++++++ src/lib/language-learner/courses/ko/b2.ts | 355 +++++++++++ src/lib/language-learner/courses/ko/c1.ts | 297 +++++++++ src/lib/language-learner/courses/ko/hangul.ts | 334 ++++++++++ src/lib/language-learner/courses/ko/index.ts | 19 + src/lib/language-learner/courses/nl/a1.ts | 558 +++++++++++++++++ src/lib/language-learner/courses/nl/a2.ts | 503 +++++++++++++++ src/lib/language-learner/courses/nl/b1.ts | 411 ++++++++++++ src/lib/language-learner/courses/nl/b2.ts | 370 +++++++++++ src/lib/language-learner/courses/nl/c1.ts | 318 ++++++++++ src/lib/language-learner/courses/nl/index.ts | 18 + src/lib/language-learner/drills/en.ts | 455 ++++++++++++++ src/lib/language-learner/drills/index.ts | 14 + src/lib/language-learner/drills/ko.ts | 382 +++++++++++ src/lib/language-learner/drills/nl.ts | 592 ++++++++++++++++++ src/lib/language-learner/drills/shared.ts | 67 ++ src/lib/language-learner/exercises.ts | 319 ++++++++++ src/lib/language-learner/hangul.test.ts | 40 ++ src/lib/language-learner/hangul.ts | 217 +++++++ src/lib/language-learner/korean-verbs.test.ts | 77 +++ src/lib/language-learner/korean-verbs.ts | 343 ++++++++++ src/lib/language-learner/modes.ts | 41 ++ src/lib/language-learner/numbers.test.ts | 37 ++ src/lib/language-learner/numbers.ts | 166 +++++ src/lib/language-learner/progress.svelte.ts | 194 ++++++ src/lib/language-learner/random.test.ts | 12 + src/lib/language-learner/random.ts | 50 ++ src/lib/language-learner/speech.ts | 43 ++ src/lib/language-learner/srs.test.ts | 28 + src/lib/language-learner/srs.ts | 76 +++ src/lib/language-learner/text.ts | 67 ++ src/lib/language-learner/types.ts | 114 ++++ src/lib/language-learner/ui/Exercise.svelte | 294 +++++++++ src/lib/language-learner/ui/Results.svelte | 79 +++ src/lib/language-learner/ui/Session.svelte | 246 ++++++++ src/lib/language-learner/ui/Speak.svelte | 22 + src/lib/projects.ts | 6 + src/lib/seo.ts | 14 +- src/lib/title.ts | 1 + src/routes/projects/+page.svelte | 1 + .../projects/language-learner/+layout.svelte | 13 + .../projects/language-learner/+page.server.ts | 20 + .../projects/language-learner/+page.svelte | 245 ++++++++ .../language-learner/[lang]/+layout.ts | 10 + .../language-learner/[lang]/+page.svelte | 211 +++++++ .../projects/language-learner/[lang]/+page.ts | 11 + .../[lang]/lesson/[lesson]/+page.svelte | 173 +++++ .../[lang]/lesson/[lesson]/+page.ts | 24 + .../[lang]/practice/[mode]/+page.svelte | 346 ++++++++++ .../[lang]/practice/[mode]/+page.ts | 37 ++ .../[lang]/words/+page.svelte | 139 ++++ .../language-learner/[lang]/words/+page.ts | 11 + .../language-learner/language-learner.css | 450 +++++++++++++ src/routes/sitemap.xml/+server.ts | 5 +- 69 files changed, 12669 insertions(+), 4 deletions(-) mode change 100755 => 100644 README.md mode change 100755 => 100644 eslint.config.js create mode 100644 src/lib/language-learner/challenges.test.ts create mode 100644 src/lib/language-learner/challenges.ts create mode 100644 src/lib/language-learner/content.test.ts create mode 100644 src/lib/language-learner/course.ts create mode 100644 src/lib/language-learner/courses/en/a1.ts create mode 100644 src/lib/language-learner/courses/en/a2.ts create mode 100644 src/lib/language-learner/courses/en/b1.ts create mode 100644 src/lib/language-learner/courses/en/b2.ts create mode 100644 src/lib/language-learner/courses/en/c1.ts create mode 100644 src/lib/language-learner/courses/en/index.ts create mode 100644 src/lib/language-learner/courses/index.ts create mode 100644 src/lib/language-learner/courses/ko/a1.ts create mode 100644 src/lib/language-learner/courses/ko/a2.ts create mode 100644 src/lib/language-learner/courses/ko/b1.ts create mode 100644 src/lib/language-learner/courses/ko/b2.ts create mode 100644 src/lib/language-learner/courses/ko/c1.ts create mode 100644 src/lib/language-learner/courses/ko/hangul.ts create mode 100644 src/lib/language-learner/courses/ko/index.ts create mode 100644 src/lib/language-learner/courses/nl/a1.ts create mode 100644 src/lib/language-learner/courses/nl/a2.ts create mode 100644 src/lib/language-learner/courses/nl/b1.ts create mode 100644 src/lib/language-learner/courses/nl/b2.ts create mode 100644 src/lib/language-learner/courses/nl/c1.ts create mode 100644 src/lib/language-learner/courses/nl/index.ts create mode 100644 src/lib/language-learner/drills/en.ts create mode 100644 src/lib/language-learner/drills/index.ts create mode 100644 src/lib/language-learner/drills/ko.ts create mode 100644 src/lib/language-learner/drills/nl.ts create mode 100644 src/lib/language-learner/drills/shared.ts create mode 100644 src/lib/language-learner/exercises.ts create mode 100644 src/lib/language-learner/hangul.test.ts create mode 100644 src/lib/language-learner/hangul.ts create mode 100644 src/lib/language-learner/korean-verbs.test.ts create mode 100644 src/lib/language-learner/korean-verbs.ts create mode 100644 src/lib/language-learner/modes.ts create mode 100644 src/lib/language-learner/numbers.test.ts create mode 100644 src/lib/language-learner/numbers.ts create mode 100644 src/lib/language-learner/progress.svelte.ts create mode 100644 src/lib/language-learner/random.test.ts create mode 100644 src/lib/language-learner/random.ts create mode 100644 src/lib/language-learner/speech.ts create mode 100644 src/lib/language-learner/srs.test.ts create mode 100644 src/lib/language-learner/srs.ts create mode 100644 src/lib/language-learner/text.ts create mode 100644 src/lib/language-learner/types.ts create mode 100644 src/lib/language-learner/ui/Exercise.svelte create mode 100644 src/lib/language-learner/ui/Results.svelte create mode 100644 src/lib/language-learner/ui/Session.svelte create mode 100644 src/lib/language-learner/ui/Speak.svelte create mode 100644 src/routes/projects/language-learner/+layout.svelte create mode 100644 src/routes/projects/language-learner/+page.server.ts create mode 100644 src/routes/projects/language-learner/+page.svelte create mode 100644 src/routes/projects/language-learner/[lang]/+layout.ts create mode 100644 src/routes/projects/language-learner/[lang]/+page.svelte create mode 100644 src/routes/projects/language-learner/[lang]/+page.ts create mode 100644 src/routes/projects/language-learner/[lang]/lesson/[lesson]/+page.svelte create mode 100644 src/routes/projects/language-learner/[lang]/lesson/[lesson]/+page.ts create mode 100644 src/routes/projects/language-learner/[lang]/practice/[mode]/+page.svelte create mode 100644 src/routes/projects/language-learner/[lang]/practice/[mode]/+page.ts create mode 100644 src/routes/projects/language-learner/[lang]/words/+page.svelte create mode 100644 src/routes/projects/language-learner/[lang]/words/+page.ts create mode 100644 src/routes/projects/language-learner/language-learner.css diff --git a/README.md b/README.md old mode 100755 new mode 100644 index 6958c8a..ed24461 --- a/README.md +++ b/README.md @@ -6,3 +6,29 @@ This is my personal website, including: - About me - Blog posts + comments - Projects like Animaldex, CatchEmAll + +## Language learner + +`/projects/language-learner` teaches Dutch and Korean (explained in English) and +English (explained in Dutch), from A1 to C1. Korean starts with a Hangul level. +It runs entirely in the browser: no database tables, migrations or API keys. + +- Course content lives in `src/lib/language-learner/courses//.ts` as + compact `target | translation | note` lines, plus markdown notes and + multiple-choice grammar questions (`prompt | answer | wrong; wrong | explanation`). + Exercises (choice, typing, sentence building, fill-in-the-blank, listening, + matching) are generated from that data. `content.test.ts` validates every lesson. +- Endless drills are generated by code in `src/lib/language-learner/drills/`: + number words, Dutch/English clock times, de/het, plurals, Dutch and Korean + conjugation (a rule-based Korean conjugator handling the ㅂ/ㄷ/르/ㅅ/ㅎ + irregulars), Korean particles and counters, Hangul reading, English irregular + verbs, comparatives and a/an. +- Challenges: a seeded daily challenge (first score of the day counts), 60-second + sprint, survival with three lives, mixed practice, level tests that mark a level + as done at 80%, and spaced-repetition reviews (simplified SM-2). +- Audio uses the browser's speech synthesis, so listening exercises appear only + when the device has a voice for the language. Romanization of Korean is + approximate Revised Romanization. +- Progress, streaks and settings are stored in `localStorage` + (`language-learner-v1`), per browser. Export/import on the overview page moves + them between devices. diff --git a/eslint.config.js b/eslint.config.js old mode 100755 new mode 100644 index a38fd6d..fa26edd --- a/eslint.config.js +++ b/eslint.config.js @@ -20,7 +20,7 @@ export default [ } }, { - files: ['**/*.svelte'], + files: ['**/*.svelte', '**/*.svelte.ts'], languageOptions: { parserOptions: { parser: ts.parser diff --git a/src/lib/language-learner/challenges.test.ts b/src/lib/language-learner/challenges.test.ts new file mode 100644 index 0000000..f8ee81d --- /dev/null +++ b/src/lib/language-learner/challenges.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest'; +import { mixed, scopeOf } from './challenges'; +import ko from './courses/ko'; +import { drills } from './drills/ko'; +import { seeded } from './random'; + +describe('challenges', () => { + it('gives beginners enough to practise', () => { + const scope = scopeOf(ko, undefined); + expect(scope.lessons[0].id).toBe('ko-vowels'); + expect( + scope.lessons.reduce((n, l) => n + l.words.length + l.sentences.length, 0) + ).toBeGreaterThanOrEqual(15); + }); + + it('limits a scope to one level', () => { + const scope = scopeOf(ko, undefined, 'b1'); + expect(scope.lessons.every((l) => l.level === 'b1')).toBe(true); + expect(scope.level).toBe(3); + }); + + it('keeps producing exercises', () => { + const next = mixed({ + course: ko, + scope: scopeOf(ko, undefined, 'a2'), + drills, + rng: seeded(1), + opts: { typing: false, audio: false } + }); + const keys = Array.from({ length: 200 }, () => next().key); + expect(new Set(keys).size).toBeGreaterThan(50); + }); +}); diff --git a/src/lib/language-learner/challenges.ts b/src/lib/language-learner/challenges.ts new file mode 100644 index 0000000..d8f0560 --- /dev/null +++ b/src/lib/language-learner/challenges.ts @@ -0,0 +1,128 @@ +import { allLessons } from './course'; +import type { DrillMode } from './drills'; +import { fromDrill, reviewExercise, type ExerciseOptions } from './exercises'; +import type { LangProgress } from './progress.svelte'; +import { pick, shuffle, type Rng } from './random'; +import { strength } from './srs'; +import type { Course, Entry, Exercise, Lesson } from './types'; + +/** CEFR level as a number for the endless drills: Hangul and A1 are 1, C1 is 5. */ +export function levelNumber(levelId: string): number { + return { hangul: 1, a1: 1, a2: 2, b1: 3, b2: 4, c1: 5 }[levelId] ?? 1; +} + +export type Scope = { lessons: Lesson[]; level: number; label: string }; + +/** + * What mixed challenges draw from. By default: completed lessons plus the next one, + * topped up in course order so beginners always have enough to practise. + */ +export function scopeOf(course: Course, p: LangProgress | undefined, scopeId = ''): Scope { + const lessons = allLessons(course); + if (scopeId === 'all') return { lessons, level: 5, label: 'Everything' }; + const chosen = course.levels.find((lv) => lv.id === scopeId); + if (chosen) + return { lessons: chosen.lessons, level: levelNumber(chosen.id), label: `${chosen.band} only` }; + + const done = lessons.filter((l) => p?.lessons[l.id]); + const next = lessons.find((l) => !p?.lessons[l.id]); + const picked = new Set([...done, ...(next ? [next] : [])]); + for (const l of lessons) { + if ([...picked].reduce((n, x) => n + x.words.length + x.sentences.length, 0) >= 15) break; + picked.add(l); + } + const result = lessons.filter((l) => picked.has(l)); + return { + lessons: result, + level: Math.max(1, ...result.map((l) => levelNumber(l.level))), + label: 'What I have learned' + }; +} + +type MixedOptions = { + course: Course; + scope: Scope; + drills: DrillMode[]; + rng: Rng; + opts: ExerciseOptions; + p?: LangProgress; + /** Grows the drill level over time, for survival. */ + ramp?: boolean; +}; + +/** An endless supply of mixed exercises: vocabulary, grammar questions and generated drills. */ +export function mixed({ course, scope, drills, rng, opts, p, ramp }: MixedOptions): () => Exercise { + const entries = scope.lessons.flatMap((l) => [...l.words, ...l.sentences]); + const grammar = scope.lessons.flatMap((l) => l.drills.map((d) => ({ d, id: l.id }))); + const recent: string[] = []; + let count = 0; + + const weightOf = (e: Entry) => 1 + (1 - strength(p?.cards[e.id])) * 3; + const total = entries.reduce((n, e) => n + weightOf(e), 0); + const weighted = () => { + let roll = rng() * total; + for (const e of entries) { + roll -= weightOf(e); + if (roll <= 0) return e; + } + return entries[entries.length - 1]; + }; + + return () => { + count++; + for (let attempt = 0; attempt < 6; attempt++) { + const roll = rng(); + let ex: Exercise; + if (roll < 0.6 || (!grammar.length && !drills.length)) { + const e = weighted(); + ex = reviewExercise(e, entries, rng, opts, strength(p?.cards[e.id])); + } else if (roll < 0.82 && grammar.length) { + const g = pick(rng, grammar); + ex = fromDrill(g.d, g.id, rng); + } else if (drills.length) { + const level = ramp ? Math.min(5, scope.level + Math.floor(count / 15)) : scope.level; + ex = pick(rng, drills).make({ rng, level, opts, course }); + } else continue; + if (!recent.includes(ex.key) || attempt === 5) { + recent.push(ex.key); + if (recent.length > 8) recent.shift(); + return ex; + } + } + return reviewExercise(weighted(), entries, rng, opts); + }; +} + +/** Due cards first, then the weakest known cards, forever. */ +export function reviews( + course: Course, + p: LangProgress, + dueIds: string[], + rng: Rng, + opts: ExerciseOptions +) { + const byId = new Map( + allLessons(course) + .flatMap((l) => [...l.words, ...l.sentences]) + .map((e) => [e.id, e]) + ); + const pool = [...byId.values()]; + const due = dueIds.map((id) => byId.get(id)).filter((e): e is Entry => !!e); + const known = Object.keys(p.cards) + .map((id) => byId.get(id)) + .filter((e): e is Entry => !!e); + const queue = shuffle(rng, due).map((e) => + reviewExercise(e, pool, rng, opts, strength(p.cards[e.id])) + ); + let extra: Entry[] = []; + const more = () => { + if (!known.length) return null; + if (!extra.length) + extra = [...known] + .sort((a, b) => strength(p.cards[a.id]) - strength(p.cards[b.id]) || rng() - 0.5) + .slice(0, 25); + const e = extra.shift()!; + return reviewExercise(e, pool, rng, opts, strength(p.cards[e.id])); + }; + return { queue, more, due: due.length, known: known.length }; +} diff --git a/src/lib/language-learner/content.test.ts b/src/lib/language-learner/content.test.ts new file mode 100644 index 0000000..3d935da --- /dev/null +++ b/src/lib/language-learner/content.test.ts @@ -0,0 +1,85 @@ +import { describe, expect, it } from 'vitest'; +import { allEntries, allLessons } from './course'; +import { lessonSession, reviewExercise } from './exercises'; +import { seeded } from './random'; +import { optionKey as normalize } from './text'; +import en from './courses/en'; +import ko from './courses/ko'; +import nl from './courses/nl'; +import { drills as nlDrills } from './drills/nl'; +import { drills as koDrills } from './drills/ko'; +import { drills as enDrills } from './drills/en'; +import type { Exercise } from './types'; + +const courses = { nl, ko, en }; +const drillSets = { nl: nlDrills, ko: koDrills, en: enDrills }; + +function checkExercise(ex: Exercise) { + if (ex.type === 'choose') { + expect(ex.options).toContain(ex.answer); + expect( + new Set(ex.options.map(normalize)).size, + `${ex.key}: duplicate options ${ex.options}` + ).toBe(ex.options.length); + expect(ex.options.length).toBeGreaterThanOrEqual(2); + expect(ex.options.length, ex.key).toBeLessThanOrEqual(4); + } + if (ex.type === 'type') expect(ex.accept.length).toBeGreaterThan(0); + if (ex.type === 'build') expect(ex.tiles.length).toBeGreaterThan(0); + if (ex.type === 'match') { + expect(new Set(ex.pairs.map((p) => normalize(p.gloss))).size, ex.key).toBe(ex.pairs.length); + expect(new Set(ex.pairs.map((p) => normalize(p.target))).size, ex.key).toBe(ex.pairs.length); + } +} + +describe.each(Object.entries(courses))('%s course', (code, course) => { + it('has unique lesson ids and content in every lesson', () => { + const ids = allLessons(course).map((l) => l.id); + expect(new Set(ids).size).toBe(ids.length); + for (const lesson of allLessons(course)) { + expect(lesson.words.length, lesson.id).toBeGreaterThanOrEqual(4); + const ids = [...lesson.words, ...lesson.sentences].map((e) => e.id); + expect( + ids.filter((id, i) => ids.indexOf(id) !== i), + lesson.id + ).toEqual([]); + expect(lesson.notes.length, lesson.id).toBeGreaterThan(50); + for (const drill of lesson.drills) { + expect(drill.wrong.map(normalize), `${lesson.id}: ${drill.prompt}`).not.toContain( + normalize(drill.answer) + ); + expect(new Set(drill.wrong.map(normalize)).size, `${lesson.id}: ${drill.prompt}`).toBe( + drill.wrong.length + ); + } + } + }); + + it('generates valid lesson sessions', () => { + for (const lesson of allLessons(course)) + for (const opts of [ + { typing: true, audio: true }, + { typing: false, audio: false } + ]) { + const session = lessonSession(course, lesson, seeded(lesson.id), opts); + expect(session.length, lesson.id).toBeGreaterThan(lesson.words.length); + session.forEach(checkExercise); + } + }); + + it('generates review exercises for every entry', () => { + const pool = allEntries(course); + const rng = seeded(code); + for (const entry of pool) + checkExercise(reviewExercise(entry, pool, rng, { typing: true, audio: true }, rng())); + }); + + it('generates endless drills at every level', () => { + const rng = seeded(`drills-${code}`); + for (const drill of drillSets[code as keyof typeof drillSets]) + for (let level = 1; level <= 5; level++) + for (let i = 0; i < 60; i++) + for (const typing of [true, false]) + checkExercise(drill.make({ rng, level, opts: { typing, audio: true }, course })); + }); +}); diff --git a/src/lib/language-learner/course.ts b/src/lib/language-learner/course.ts new file mode 100644 index 0000000..ea95ab1 --- /dev/null +++ b/src/lib/language-learner/course.ts @@ -0,0 +1,111 @@ +import { normalize } from './text'; +import type { Course, Drill, Entry, EntryKind, LangCode, Lesson, Level } from './types'; + +type LessonSource = { + id: string; + title: string; + summary: string; + notes?: string; + /** One per line: `target | gloss | note`. Separate alternatives with `;`. */ + words?: string; + sentences?: string; + /** Hangul letters: `letter | romanization | note`. */ + script?: string; + /** One per line: `prompt | answer | wrong; wrong; wrong | explanation`. */ + drills?: string; +}; + +const lines = (source = '') => + source + .split('\n') + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith('#')); + +const alternatives = (value: string) => + value + .split(';') + .map((part) => part.trim()) + .filter(Boolean); + +function entries( + source: string | undefined, + kind: EntryKind, + lesson: string, + code: LangCode +): Entry[] { + return lines(source).map((line) => { + const [target, gloss, note] = line.split('|').map((part) => part.trim()); + if (!target || !gloss) throw new Error(`Malformed ${kind} in ${code}/${lesson}: ${line}`); + const accepted = alternatives(target); + return { + id: `${code}:${kind[0]}:${normalize(accepted[0])}`, + kind, + target: accepted, + gloss: alternatives(gloss), + note: note || undefined, + lesson + }; + }); +} + +function drills(source: string | undefined, lesson: string): Drill[] { + return lines(source).map((line) => { + const [prompt, answer, wrong, explain] = line.split('|').map((part) => part.trim()); + if (!prompt || !answer || !wrong) throw new Error(`Malformed drill in ${lesson}: ${line}`); + return { prompt, answer, wrong: alternatives(wrong), explain: explain || undefined }; + }); +} + +/** Strip the shared indentation of template literals so markdown renders correctly. */ +function dedent(text = ''): string { + const rows = text.replace(/^\n/, '').replace(/\s+$/, '').split('\n'); + const indent = Math.min(...rows.filter((r) => r.trim()).map((r) => r.match(/^\s*/)![0].length)); + return rows.map((r) => r.slice(Number.isFinite(indent) ? indent : 0)).join('\n'); +} + +export const lesson = (source: LessonSource) => source; + +export const level = ( + id: string, + band: string, + title: string, + description: string, + lessons: LessonSource[] +) => ({ + id, + band, + title, + description, + lessons +}); + +export function course(meta: Omit, levels: ReturnType[]): Course { + return { + ...meta, + levels: levels.map((lv): Level => ({ + ...lv, + lessons: lv.lessons.map((src): Lesson => ({ + id: src.id, + title: src.title, + summary: src.summary, + notes: dedent(src.notes), + words: [ + ...entries(src.script, 'script', src.id, meta.code), + ...entries(src.words, 'word', src.id, meta.code) + ], + sentences: entries(src.sentences, 'sentence', src.id, meta.code), + drills: drills(src.drills, src.id), + level: lv.id + })) + })) + }; +} + +export const allLessons = (c: Course) => c.levels.flatMap((lv) => lv.lessons); + +export const allEntries = (c: Course) => + allLessons(c).flatMap((ls) => [...ls.words, ...ls.sentences]); + +export function findLesson(c: Course, id: string) { + return allLessons(c).find((ls) => ls.id === id); +} diff --git a/src/lib/language-learner/courses/en/a1.ts b/src/lib/language-learner/courses/en/a1.ts new file mode 100644 index 0000000..4e877df --- /dev/null +++ b/src/lib/language-learner/courses/en/a1.ts @@ -0,0 +1,567 @@ +import { lesson, level } from '../../course'; + +export default level( + 'a1', + 'A1', + 'Beginner', + 'Begroeten, jezelf voorstellen, getallen, familie en de present simple.', + [ + lesson({ + id: 'en-hello', + title: 'Hello!', + summary: 'Begroeten, bedanken en afscheid nemen', + notes: ` + **Hi** en **hello** kun je altijd gebruiken. **Hey** is heel informeel. + + | Engels | wanneer | + |---|---| + | Good morning | tot ongeveer 12:00 | + | Good afternoon | 12:00–18:00 | + | Good evening | 's avonds, als begroeting | + | Good night | alleen bij afscheid of slapengaan | + + **How are you?** is vaak gewoon een begroeting. Antwoord kort: *Fine, thanks. And you?* + + Let op: **please** is "alsjeblieft" bij een verzoek, maar als je iets aangeeft zeg je **here you are**. + "Graag gedaan" is **you're welcome** of **no problem**. + `, + words: ` + hello; hi | hallo; hoi + goodbye; bye | tot ziens; doei + good morning | goedemorgen + good afternoon | goedemiddag + good evening | goedenavond + good night | welterusten; goedenacht + see you later | tot later + please | alsjeblieft (bij een verzoek) + here you are | alsjeblieft (bij het aangeven) + thank you; thanks | dank je wel; bedankt + you're welcome | graag gedaan + sorry | sorry + excuse me | pardon + yes | ja + no | nee + `, + sentences: ` + Hi, how are you? | Hoi, hoe gaat het? + I'm fine, thanks. And you? | Goed, dank je. En met jou? + Nice to meet you. | Leuk je te ontmoeten. + See you tomorrow! | Tot morgen! + A coffee, please. | Een koffie, alsjeblieft. + Thank you very much. | Heel erg bedankt. + Excuse me, where is the station? | Pardon, waar is het station? + `, + drills: ` + It is 21:00 and you arrive at a party: | Good evening | Good night; Good morning; Good afternoon | Good night zeg je alleen bij afscheid. + You give someone their coffee: | Here you are | Please; You're welcome; Thank you + Someone says "Thank you!" You answer: | You're welcome | Please; Here you are; Excuse me + Hoe gaat het? | How are you? | How do you go?; How goes it?; How is it going you? + ` + }), + lesson({ + id: 'en-to-be', + title: 'I am, you are', + summary: 'Persoonlijke voornaamwoorden en "to be"', + notes: ` + | | to be | kort | + |---|---|---| + | I | am | I'm | + | you | are | you're | + | he / she / it | is | he's / she's / it's | + | we / you / they | are | we're / you're / they're | + + **You** is zowel jij, u als jullie. + + Ontkenning: **not** na de vorm van *be*: *I'm **not** tired. She **isn't** here.* + Vraag: zet *be* vooraan: ***Are** you Dutch? **Is** he a teacher?* + + Let op: **ik ben 20** is ***I am 20*** (niet ~~I have 20~~), en **ik heb het koud** is ***I am cold***. + Een beroep krijgt altijd **a/an**: *I'm **a** student. She's **an** engineer.* + `, + words: ` + I | ik + you | jij; u; jullie + he | hij + she | zij (enkelvoud) + it | het + we | wij + they | zij (meervoud) + am | ben + is | is + are | bent; zijn + name | naam + student | student + teacher | leraar; lerares + from | uit; van + Dutch | Nederlands; Nederlander + `, + sentences: ` + My name is Emma. | Mijn naam is Emma. + I'm from the Netherlands. | Ik kom uit Nederland. + Are you a student? | Ben jij student? + She's a teacher. | Zij is lerares. + We aren't tired. | We zijn niet moe. + They're my friends. | Het zijn mijn vrienden. + I'm twenty years old. | Ik ben twintig jaar. + Is it cold outside? | Is het koud buiten? + `, + drills: ` + She ___ from Amsterdam. | is | are; am; be + They ___ my parents. | are | is; am; be + I ___ a student. | am | is; are; be + Ik ben student: | I'm a student. | I'm student.; I have a student.; I'm the student. | Een beroep krijgt a/an. + Ik ben 30: | I'm 30. | I have 30.; I have 30 years.; I'm 30 years. + Heb je het koud? | Are you cold? | Do you have cold?; Have you cold?; Is you cold? + ` + }), + lesson({ + id: 'en-numbers', + title: 'One, two, three', + summary: 'Getallen, leeftijd en telefoonnummers', + notes: ` + | | | | | + |---|---|---|---| + | 1 one | 6 six | 11 eleven | 16 sixteen | + | 2 two | 7 seven | 12 twelve | 17 seventeen | + | 3 three | 8 eight | 13 thirteen | 18 eighteen | + | 4 four | 9 nine | 14 fourteen | 19 nineteen | + | 5 five | 10 ten | 15 fifteen | 20 twenty | + + Tientallen: twenty, thirty, **forty** (zonder u!), fifty, sixty, seventy, eighty, ninety. + + In het Engels komen **tientallen eerst**, met een streepje: **twenty-one** (niet "one-and-twenty"), **forty-five**. + + *-teen* en *-ty* klinken verschillend: thir**teen** (klemtoon achteraan) vs **thir**ty (klemtoon vooraan). + + Telefoonnummers cijfer voor cijfer, 0 is **"oh"**: 06 = *oh six*. Twee dezelfde: *double four*. + `, + words: ` + one | een + two | twee + three | drie + four | vier + five | vijf + eleven | elf + twelve | twaalf + thirteen | dertien + fifteen | vijftien + twenty | twintig + thirty | dertig + forty | veertig + fifty | vijftig + hundred | honderd + thousand | duizend + old | oud + `, + sentences: ` + How old are you? | Hoe oud ben je? + I'm twenty-five. | Ik ben vijfentwintig. + What's your phone number? | Wat is je telefoonnummer? + It costs forty euros. | Het kost veertig euro. + I have two brothers. | Ik heb twee broers. + There are thirty students in my class. | Er zitten dertig leerlingen in mijn klas. + `, + drills: ` + 45 | forty-five | fourty-five; five-and-forty; fifty-four + 14 | fourteen | forteen; fourty; forty + 90 | ninety | ninty; nineteen; nineth + 12 | twelve | twelf; twoteen; eleven + 32 | thirty-two | two-and-thirty; thirteen-two; twenty-three + ` + }), + lesson({ + id: 'en-family', + title: 'My family', + summary: "Familie, bezittelijke voornaamwoorden en 's", + notes: ` + ### Bezittelijke voornaamwoorden + | | | + |---|---| + | my | mijn | + | your | jouw, uw, jullie | + | his | zijn | + | her | haar | + | its | zijn/haar (dingen, dieren) | + | our | ons, onze | + | their | hun | + + ### Bezit met 's + *Anna**'s** brother* — de broer van Anna. Meervoud op -s: alleen een apostrof: *my parents**'** house*. + + **its** (bezit, zonder apostrof) ≠ **it's** (= it is). + + ### Have got + In Brits Engels zeg je vaak **have got** voor bezit: *I**'ve got** a sister. **Has** she **got** kids?* Beide zijn goed: *I have a sister.* + `, + words: ` + family | familie; gezin + mother; mum | moeder; mama + father; dad | vader; papa + parents | ouders + sister | zus + brother | broer + son | zoon + daughter | dochter + child | kind + children | kinderen + husband | echtgenoot + wife | echtgenote + grandmother | oma + grandfather | opa + aunt | tante + uncle | oom + cousin | neef; nicht (kind van oom/tante) + `, + sentences: ` + This is my mother. | Dit is mijn moeder. + I've got two sisters. | Ik heb twee zussen. + Have you got any children? | Heb jij kinderen? + Her husband is Scottish. | Haar man is Schots. + My brother's name is Tom. | De naam van mijn broer is Tom. + Our parents live in Utrecht. | Onze ouders wonen in Utrecht. + Their house is very big. | Hun huis is erg groot. + `, + drills: ` + Dit is de auto van Mark: | This is Mark's car. | This is the car of Mark.; This is Marks car.; This is Mark car. + Is that ___ bag? (haar) | her | his; she; hers + The dog wants ___ food. | its | it's; his; it + Het huis van mijn ouders: | my parents' house | my parent's house; my parents house; the house from my parents + She ___ got a brother. | has | have; is; does + ` + }), + lesson({ + id: 'en-present-simple', + title: 'I work, she works', + summary: 'De present simple en de -s bij he/she/it', + notes: ` + De **present simple** gebruik je voor gewoontes, feiten en dingen die altijd zo zijn. + + | | work | + |---|---| + | I / you / we / they | work | + | **he / she / it** | work**s** | + + Vergeet de **-s** niet! Spelling: *watch → watch**es**, go → go**es**, study → stud**ies**, have → **has***. + + ### Ontkenning en vraag met do / does + - *I **don't** work on Sundays. She **doesn't** work.* + - ***Do** you like coffee? **Does** he live here?* + + Na **does/doesn't** geen -s meer: *Does she **work**?* (niet ~~works~~). + + Nederlanders zeggen vaak ~~Work you here?~~ — in het Engels **altijd** met do/does: *Do you work here?* + `, + words: ` + work | werken + live | wonen; leven + like | leuk vinden + eat | eten + drink | drinken + go | gaan + get up | opstaan + watch | kijken (naar) + play | spelen + read | lezen + study | studeren + every day | elke dag + usually | meestal + always | altijd + never | nooit + `, + sentences: ` + I work in an office. | Ik werk op een kantoor. + She lives in London. | Zij woont in Londen. + He doesn't eat meat. | Hij eet geen vlees. + Do you like football? | Hou je van voetbal? + My sister studies medicine. | Mijn zus studeert geneeskunde. + We usually get up at seven. | We staan meestal om zeven uur op. + Does he play the guitar? | Speelt hij gitaar? + `, + drills: ` + She ___ in a bank. (work) | works | work; working; is work + He ___ TV every evening. (watch) | watches | watchs; watch; watching + My brother ___ history. (study) | studies | studys; study; studyes + ___ you speak English? | Do | Does; Are; Speak + She doesn't ___ coffee. (like) | like | likes; liking; to like | Na doesn't geen -s. + Werk je hier? | Do you work here? | Work you here?; Are you work here?; Does you work here? + ` + }), + lesson({ + id: 'en-questions', + title: 'Who, what, where?', + summary: 'Vraagwoorden en vragen stellen', + notes: ` + | | | + |---|---| + | who | wie | + | what | wat | + | where | waar | + | when | wanneer | + | why | waarom | + | how | hoe | + | which | welke | + | whose | van wie | + | how much | hoeveel (ontelbaar) | + | how many | hoeveel (telbaar) | + | how often | hoe vaak | + + Volgorde: **vraagwoord + do/does + onderwerp + werkwoord**: *Where **do** you live?* + Met *be* of een hulpwerkwoord (can, will): gewoon omdraaien: *Where **are** you? What **can** I do?* + + Uitzondering: als *who/what* het onderwerp is, geen *do*: ***Who lives** here?* (niet ~~Who does live here?~~). + `, + words: ` + who | wie + what | wat + where | waar + when | wanneer + why | waarom + how | hoe + which | welke + whose | van wie + how much | hoeveel (ontelbaar) + how many | hoeveel (telbaar) + how often | hoe vaak + because | omdat; want + `, + sentences: ` + Where do you live? | Waar woon je? + What do you do? | Wat doe je (voor werk)? + Why are you late? | Waarom ben je te laat? + How much is this? | Hoeveel kost dit? + How many brothers have you got? | Hoeveel broers heb je? + Who lives here? | Wie woont hier? + Whose phone is this? | Van wie is deze telefoon? + How often do you go to the gym? | Hoe vaak ga je naar de sportschool? + `, + drills: ` + ___ money do you have? | How much | How many; How often; What many + ___ people are there? | How many | How much; How often; Who many + Waar woon je? | Where do you live? | Where live you?; Where you live?; Where are you live? + ___ phone is this? – It's Tom's. | Whose | Who's; Who; Which's + Wie wil koffie? | Who wants coffee? | Who does want coffee?; Who want coffee?; Whom wants coffee? + ` + }), + lesson({ + id: 'en-time', + title: 'What time is it?', + summary: 'Dagen, maanden en de klok (pas op met "half")', + notes: ` + In het Engels krijgen dagen en maanden een **hoofdletter**: *Monday, January*. + + ### De klok: pas op! + | tijd | Engels | niet | + |---|---|---| + | 2:30 | **half past two** | ~~half three~~ (Brits informeel = 3:30!) | + | 2:15 | quarter past two | | + | 2:45 | quarter to three | | + | 2:20 | twenty past two | | + | 2:40 | twenty to three | | + + Het Engelse *half past two* is **half drie**. Je kunt ook gewoon de cijfers zeggen: *two thirty*. + **a.m.** = voor 12 uur 's middags, **p.m.** = erna. De 24-uursklok is ongebruikelijk in spreektaal. + + Voorzetsels: **at** + tijd (*at 3 o'clock*), **on** + dag (*on Monday*), **in** + maand/jaar (*in May, in 2026*). + `, + words: ` + Monday | maandag + Tuesday | dinsdag + Wednesday | woensdag + Thursday | donderdag + Friday | vrijdag + Saturday | zaterdag + Sunday | zondag + weekend | weekend + month | maand + year | jaar + hour | uur + minute | minuut + half past | half (+ volgend uur in het Nederlands) + quarter | kwartier + o'clock | uur (hele uren) + today | vandaag + tomorrow | morgen + yesterday | gisteren + `, + sentences: ` + What time is it? | Hoe laat is het? + It's half past seven. | Het is half acht. + The film starts at nine o'clock. | De film begint om negen uur. + I work from Monday to Friday. | Ik werk van maandag tot en met vrijdag. + My birthday is in June. | Mijn verjaardag is in juni. + See you on Saturday! | Tot zaterdag! + It's quarter to four. | Het is kwart voor vier. + `, + drills: ` + Half drie (2:30): | half past two | half three; half past three; half to three | Engels telt vanaf het vorige uur. + Kwart over negen: | quarter past nine | quarter to nine; quarter over nine; quarter past ten + ___ Monday | on | in; at; by + ___ 6 o'clock | at | on; in; by + ___ July | in | on; at; by + ` + }), + lesson({ + id: 'en-food', + title: 'Food and drink', + summary: 'Eten, drinken, a/an/some/any', + notes: ` + ### a / an + **a** voor een medeklinker-**klank**, **an** voor een klinker-**klank**: *a banana, **an** apple, **an** hour* (h stil), *a university* ("joe"). + + ### some / any + - **some** in bevestigende zinnen: *I'd like **some** water.* + - **any** in vragen en ontkenningen: *Have you got **any** milk? There isn't **any** bread.* + - **some** in aanbiedingen en verzoeken: *Would you like **some** tea?* + + ### Ontelbaar + Sommige woorden zijn in het Engels ontelbaar (geen *a*, geen meervoud): *water, bread, rice, milk, advice, information, furniture, news*. + Dus: ~~an advice~~ → *some advice / a piece of advice*. + + **I'd like** (= I would like) is de beleefde manier om iets te bestellen. + `, + words: ` + bread | brood + cheese | kaas + butter | boter + egg | ei + apple | appel + banana | banaan + water | water + coffee | koffie + tea | thee + milk | melk + beer | bier + breakfast | ontbijt + lunch | lunch + dinner | avondeten + hungry | hongerig + thirsty | dorstig + delicious | heerlijk; lekker + `, + sentences: ` + I'm hungry. | Ik heb honger. + Are you thirsty? | Heb je dorst? + I'd like a cheese sandwich, please. | Ik wil graag een broodje kaas. + Would you like some tea? | Wil je wat thee? + We haven't got any milk. | We hebben geen melk. + This soup is delicious! | Deze soep is heerlijk! + Can I have the bill, please? | Mag ik de rekening, alstublieft? + `, + drills: ` + ___ apple | an | a; the an; some + ___ hour | an | a; one; some | De h is stil. + ___ university | a | an; one; the an | "University" begint met een j-klank. + Is there ___ sugar? | any | some; a; many + Ik heb honger: | I'm hungry. | I have hunger.; I am hunger.; I have hungry. + I need some ___. (advies) | advice | advices; an advice; advise + ` + }), + lesson({ + id: 'en-town', + title: 'In town', + summary: 'There is / there are, plekken en de weg vragen', + notes: ` + ### There is / there are + - **There's** a bakery on the corner. (enkelvoud) + - **There are** two supermarkets. (meervoud) + - **Is there** a bank near here? **Are there** any shops? + + Nederlanders zeggen vaak ~~It has a nice park~~ voor "Er is een mooi park" — dat is fout. Gebruik ***There's** a nice park*. + + ### De weg wijzen + | | | + |---|---| + | go straight on | ga rechtdoor | + | turn left / right | sla linksaf / rechtsaf | + | take the first left | neem de eerste links | + | on the corner | op de hoek | + | opposite | tegenover | + | next to | naast | + | between | tussen | + `, + words: ` + street | straat + station | station + shop | winkel + supermarket | supermarkt + bank | bank (geld) + bakery | bakkerij + square | plein + bridge | brug + park | park + hospital | ziekenhuis + left | links + right | rechts + straight on | rechtdoor + corner | hoek + opposite | tegenover + near | dichtbij + far | ver + `, + sentences: ` + Is there a bank near here? | Is er een bank hier in de buurt? + There's a café opposite the station. | Er is een café tegenover het station. + There are a lot of bikes in Amsterdam. | Er zijn veel fietsen in Amsterdam. + Go straight on and turn left. | Ga rechtdoor en sla linksaf. + The hospital is next to the park. | Het ziekenhuis is naast het park. + Is it far from here? | Is het ver hiervandaan? + Excuse me, how do I get to the museum? | Pardon, hoe kom ik bij het museum? + `, + drills: ` + ___ a nice park in my town. | There's | It has; There are; It's + ___ three bedrooms in the house. | There are | There's; It has; They are + Sla rechtsaf: | turn right | go right off; turn rightoff; take right + The bank is ___ the post office. (naast) | next to | beside of; near of; next + ` + }), + lesson({ + id: 'en-adjectives', + title: 'Big, red, beautiful', + summary: 'Kleuren en bijvoeglijke naamwoorden', + notes: ` + Engelse bijvoeglijke naamwoorden veranderen **nooit**: geen -e, geen meervoud. + *a **big** house, the **big** houses, a **red** car* (niet ~~a rede car~~ of ~~bigs houses~~). + + Ze staan **voor** het zelfstandig naamwoord: *a **new** phone* — of na *be*: *My phone is **new**.* + + Versterken: **very** (erg), **really** (echt), **quite** (behoorlijk), **too** (te): *It's **too** expensive.* + + Volgorde bij meerdere: mening – grootte – leeftijd – kleur: *a **beautiful big old red** car*. + `, + words: ` + red | rood + blue | blauw + yellow | geel + green | groen + black | zwart + white | wit + orange | oranje + grey | grijs + brown | bruin + pink | roze + big | groot + small | klein + beautiful | mooi + ugly | lelijk + new | nieuw + old | oud + expensive | duur + cheap | goedkoop + `, + sentences: ` + I've got a red bike. | Ik heb een rode fiets. + The house is very big. | Het huis is erg groot. + These shoes are too expensive. | Deze schoenen zijn te duur. + She has beautiful eyes. | Ze heeft prachtige ogen. + It's a small old car. | Het is een kleine oude auto. + My new phone is black. | Mijn nieuwe telefoon is zwart. + `, + drills: ` + de grote huizen: | the big houses | the bigs houses; the bige houses; the houses big + een nieuwe auto: | a new car | a newe car; a car new; an new car + Het is te duur: | It's too expensive. | It's to expensive.; It's two expensive.; It's also expensive. + a ___ car (mooi, oud) | beautiful old | old beautiful; beautifully old; beautifuls old + ` + }) + ] +); diff --git a/src/lib/language-learner/courses/en/a2.ts b/src/lib/language-learner/courses/en/a2.ts new file mode 100644 index 0000000..f369f34 --- /dev/null +++ b/src/lib/language-learner/courses/en/a2.ts @@ -0,0 +1,419 @@ +import { lesson, level } from '../../course'; + +export default level( + 'a2', + 'A2', + 'Elementary', + 'Present continuous, de verleden tijd, toekomst, vergelijken en modale werkwoorden.', + [ + lesson({ + id: 'en-continuous', + title: 'I am working', + summary: 'Present continuous versus present simple', + notes: ` + **Present continuous** = *am/is/are + werkwoord-ing*: iets wat **nu** of **tijdelijk** gebeurt. + *I'**m working** right now. She'**s living** with her parents this year.* + + **Present simple** = gewoontes en feiten: *I **work** in Utrecht. She **lives** in Leeds.* + + | nu / tijdelijk | gewoonte / altijd | + |---|---| + | now, right now, at the moment, today, this week | always, usually, every day, often, never | + + Nederlands heeft hier vaak geen verschil: "ik werk" kan beide zijn. Denk aan "ik **ben aan het** werken" → continuous. + + Sommige werkwoorden (toestanden) krijgen bijna nooit -ing: *know, like, want, need, understand, believe, own*. + ~~I'm knowing~~ → *I **know***. + + Spelling: *make → making, swim → swimming, lie → lying*. + `, + words: ` + right now | nu; op dit moment + at the moment | op het moment + this week | deze week + wait | wachten + rain | regenen + cook | koken + listen | luisteren + look for | zoeken naar + wear | dragen (kleding) + understand | begrijpen + know | weten; kennen + want | willen + need | nodig hebben + `, + sentences: ` + What are you doing? | Wat ben je aan het doen? + I'm cooking dinner. | Ik ben het avondeten aan het koken. + It's raining again. | Het regent weer. + She's wearing a blue dress today. | Ze draagt vandaag een blauwe jurk. + I usually walk to work, but today I'm taking the bus. | Meestal loop ik naar mijn werk, maar vandaag neem ik de bus. + I don't understand the question. | Ik begrijp de vraag niet. + We're looking for the station. | We zoeken het station. + `, + drills: ` + Look! It ___. | is snowing | snows; snowing; is snow + I ___ to work every day. | cycle | am cycling; cycling; cycles + She ___ the answer. (weten) | knows | is knowing; know; knowing | Know krijgt geen -ing. + Wat ben je aan het doen? | What are you doing? | What do you doing?; What are you do?; What you are doing? + make + ing | making | makeing; makking; maken + swim + ing | swimming | swiming; swimmming; swimeing + ` + }), + lesson({ + id: 'en-past-simple', + title: 'Yesterday I went', + summary: 'Past simple: regelmatig, onregelmatig en did', + notes: ` + De **past simple** gebruik je voor iets dat **afgelopen** is, op een bepaald moment in het verleden: *yesterday, last week, in 2019, two days ago*. + + ### Regelmatig: -ed + *work → work**ed**, live → live**d**, stop → stop**ped**, study → stud**ied***. + + ### Onregelmatig + Veel gebruikte werkwoorden zijn onregelmatig: *go → **went**, have → **had**, see → **saw**, buy → **bought***. Oefen ze met de drill **Irregular verbs**. + + ### Vragen en ontkenning met did + - *I **didn't** go.* (niet ~~didn't went~~) + - ***Did** you **see** it?* (niet ~~Did you saw~~) + + **Ago** = geleden, en komt **na** de tijd: *three years **ago***. + *Be* is was/were: *I **was** tired. They **were** late.* + `, + words: ` + yesterday | gisteren + last week | vorige week + last year | vorig jaar + ago | geleden + went | ging + had | had + saw | zag + bought | kocht + ate | at + came | kwam + was | was + were | waren + did | deed + visit | bezoeken + stay | blijven; logeren + `, + sentences: ` + I went to London last year. | Ik ging vorig jaar naar Londen. + We stayed in a small hotel. | We verbleven in een klein hotel. + She bought a new laptop yesterday. | Ze kocht gisteren een nieuwe laptop. + Did you see the match? | Heb je de wedstrijd gezien? + I didn't have time. | Ik had geen tijd. + They were very tired. | Ze waren heel moe. + I met him three years ago. | Ik heb hem drie jaar geleden ontmoet. + `, + drills: ` + I ___ to Paris last summer. (go) | went | goed; gone; go + She ___ a cake yesterday. (make) | made | maked; make; maden + I didn't ___ him. (see) | see | saw; seen; seeing | Na didn't: het hele werkwoord. + ___ you enjoy the film? | Did | Do; Were; Have + drie jaar geleden: | three years ago | ago three years; before three years; three years before + stop → past | stopped | stoped; stopt; stopd + ` + }), + lesson({ + id: 'en-future', + title: 'Going to and will', + summary: 'De toekomst: going to, will en de present continuous', + notes: ` + | vorm | gebruik | voorbeeld | + |---|---|---| + | **going to** | plannen, iets wat je al besloten hebt | *I'm going to buy a car.* | + | **will** | een besluit op dit moment, beloftes, voorspellingen | *I'll help you! It will rain.* | + | **present continuous** | afspraken met tijd en plaats | *I'm meeting Sam at 8.* | + + Spontaan besluit: telefoon gaat → *I'**ll** get it!* (niet ~~I'm going to get it~~). + Voorspelling met bewijs: *Look at those clouds! It's **going to** rain.* + + **Shall I/we …?** voor voorstellen: ***Shall** we go?* — Zullen we gaan? + Let op: Nederlands "ik wil" is **I want**, niet *I will*! + `, + words: ` + tomorrow | morgen + next week | volgende week + next year | volgend jaar + soon | binnenkort + later | later + plan | plan; plannen + going to | gaan (toekomst) + will | zullen + shall | zullen (voorstel) + probably | waarschijnlijk + maybe | misschien + promise | beloven + `, + sentences: ` + I'm going to learn Korean next year. | Ik ga volgend jaar Koreaans leren. + I'll call you tomorrow. | Ik bel je morgen. + It's going to rain. | Het gaat regenen. + Shall we get something to eat? | Zullen we iets gaan eten? + I'm meeting my sister at six. | Ik zie mijn zus om zes uur. + I think she'll love it. | Ik denk dat ze het geweldig zal vinden. + I promise I won't be late. | Ik beloof dat ik niet te laat kom. + `, + drills: ` + The phone is ringing. – I ___ it! | 'll get | 'm going to get; get; 'm getting + We've booked the tickets. We ___ to Spain. | 're going to go | 'll go; go; shall go + ___ we dance? (Zullen we) | Shall | Will; Want; Going + Ik wil een koffie: | I want a coffee. | I will a coffee.; I will have want a coffee.; I wish a coffee. + will not → | won't | willn't; wont; will'nt + ` + }), + lesson({ + id: 'en-comparing', + title: 'Bigger and the biggest', + summary: 'Trappen van vergelijking', + notes: ` + | | vergrotende trap | overtreffende trap | + |---|---|---| + | kort (1 lettergreep) | + **-er**: tall**er** | **the** + **-est**: the tall**est** | + | op -y | **-ier**: happ**ier** | the happ**iest** | + | lang (2+ lettergrepen) | **more** beautiful | **the most** beautiful | + | onregelmatig | good → **better**, bad → **worse**, far → **further** | **the best**, **the worst**, **the furthest** | + + Spelling: *big → bi**gg**er, hot → ho**tt**er*. + + **than** = dan: *She's older **than** me.* (niet ~~then~~!) + **as … as** = net zo … als: *He's **as** tall **as** his dad.* + `, + words: ` + bigger | groter + smaller | kleiner + better | beter + best | best + worse | slechter + worst | slechtst + more | meer + most | meest + than | dan + as … as | net zo … als + tall | lang (persoon) + fast | snel + easy | makkelijk + difficult | moeilijk + `, + sentences: ` + My brother is taller than me. | Mijn broer is langer dan ik. + This is the best pizza in town. | Dit is de beste pizza van de stad. + English is easier than Dutch. | Engels is makkelijker dan Nederlands. + It's the most beautiful city in the world. | Het is de mooiste stad ter wereld. + Today is worse than yesterday. | Vandaag is erger dan gisteren. + She's as clever as her sister. | Ze is net zo slim als haar zus. + `, + drills: ` + He is ___ than me. (old) | older | more old; oldest; elder then + This is ___ restaurant in town. (good) | the best | the better; the goodest; the most good + The test was ___ than I thought. (difficult) | more difficult | difficulter; most difficult; more difficulter + groter dan: | bigger than | biger than; bigger then; more big than + She's ___ than her sister. (happy) | happier | happyer; more happy; happiest + ` + }), + lesson({ + id: 'en-modals', + title: 'Can, must, should', + summary: 'Modale werkwoorden', + notes: ` + | | betekenis | + |---|---| + | can / can't | kunnen, mogen (informeel) | + | could | kon, zou kunnen (beleefd) | + | must / mustn't | moeten (eigen gevoel) / **niet mogen** | + | have to / don't have to | moeten (regel van buitenaf) / **niet hoeven** | + | should / shouldn't | zouden moeten (advies) | + | may | mogen (formeel), misschien | + + Na een modaal werkwoord: **geen to en geen -s**: *She **can swim**.* (niet ~~cans~~, niet ~~can to swim~~). + + **Grote valkuil**: *mustn't* = niet mogen, *don't have to* = niet hoeven. + - *You **mustn't** smoke here.* — Je mag hier niet roken. + - *You **don't have to** come.* — Je hoeft niet te komen. + `, + words: ` + can | kunnen + could | kon; zou kunnen + must | moeten + mustn't | niet mogen + have to | moeten + don't have to | niet hoeven + should | zou moeten + may | mogen; misschien + allowed | toegestaan + rule | regel + advice | advies + `, + sentences: ` + Can you swim? | Kun je zwemmen? + Could you help me, please? | Zou je me kunnen helpen? + You mustn't park here. | Je mag hier niet parkeren. + I have to work on Saturday. | Ik moet zaterdag werken. + You don't have to pay. | Je hoeft niet te betalen. + You should see a doctor. | Je zou naar een dokter moeten gaan. + May I come in? | Mag ik binnenkomen? + `, + drills: ` + Je hoeft niet te komen: | You don't have to come. | You mustn't come.; You haven't to come.; You don't must come. + Je mag hier niet roken: | You mustn't smoke here. | You don't have to smoke here.; You mustn't to smoke here.; You not must smoke here. + She ___ speak three languages. | can | cans; can to; is can + You look ill. You ___ go home. (advies) | should | must to; would; shall + ___ you open the window, please? (beleefd) | Could | Must; Should; Shall + ` + }), + lesson({ + id: 'en-prepositions', + title: 'In, on, at', + summary: 'Voorzetsels van tijd en plaats', + notes: ` + ### Tijd + | at | on | in | + |---|---|---| + | kloktijd: at 5 o'clock | dagen: on Monday | maanden: in May | + | at night, at the weekend (BrE) | datums: on 3 March | jaren: in 2026 | + | at Christmas | on my birthday | seizoenen: in (the) summer | + | | | dagdelen: in the morning | + + ### Plaats + - **at** een punt/adres: *at home, at work, at school, at the station, at 10 Baker Street* + - **in** een ruimte/stad/land: *in the kitchen, in London, in the car* + - **on** een oppervlak/straat/vervoer: *on the table, on the wall, on the bus, on Oxford Street* + + Let op: *in the car* maar *on the bus/train/plane*. + `, + words: ` + at | om; bij; op + on | op + in | in + at home | thuis + at work | op het werk + at night | 's nachts + in the morning | 's ochtends + on the bus | in de bus + on the wall | aan de muur + during | tijdens + until | tot + for | voor; gedurende + `, + sentences: ` + I'm at home. | Ik ben thuis. + She lives in Rotterdam. | Ze woont in Rotterdam. + The meeting is on Tuesday at ten. | De vergadering is dinsdag om tien uur. + I was born in 1998. | Ik ben in 1998 geboren. + There's a picture on the wall. | Er hangt een schilderij aan de muur. + I read on the train. | Ik lees in de trein. + We go skiing in winter. | We gaan in de winter skiën. + `, + drills: ` + ___ Friday | on | in; at; by + ___ 2020 | in | on; at; by + ___ 7 p.m. | at | on; in; by + ___ the morning | in | on; at; by + I'm ___ work. | at | on; in; by + I left my bag ___ the bus. | on | in; at; by + There's a clock ___ the wall. | on | at; in; by + ` + }), + lesson({ + id: 'en-quantity', + title: 'Much, many, a lot of', + summary: 'Telbaar en ontelbaar', + notes: ` + | | telbaar (books, people) | ontelbaar (water, money, time) | + |---|---|---| + | veel (vraag/ontkenning) | **many** | **much** | + | veel (bevestigend) | a lot of / lots of | a lot of / lots of | + | een paar / een beetje | **a few** | **a little** | + | weinig | few | little | + | minder | **fewer** | **less** | + | hoeveel | how **many** | how **much** | + + *There aren't **many** people. I don't have **much** time. I've got **a lot of** friends.* + + **Money, time, information, advice, furniture, luggage, news, homework** zijn ontelbaar in het Engels. + `, + words: ` + much | veel (ontelbaar) + many | veel (telbaar) + a lot of | veel + a few | een paar + a little | een beetje + few | weinig (telbaar) + little | weinig (ontelbaar) + money | geld + time | tijd + information | informatie + luggage | bagage + furniture | meubels + homework | huiswerk + `, + sentences: ` + I don't have much time. | Ik heb niet veel tijd. + How many people are coming? | Hoeveel mensen komen er? + How much luggage do you have? | Hoeveel bagage heb je? + There are a few problems. | Er zijn een paar problemen. + Can I have a little milk? | Mag ik een beetje melk? + She's got a lot of friends. | Ze heeft veel vrienden. + We need more information. | We hebben meer informatie nodig. + `, + drills: ` + How ___ money do you need? | much | many; lot; few + How ___ brothers do you have? | many | much; lot; little + I have ___ questions. (een paar) | a few | a little; few of; a bit + We've got ___ time left. (een beetje) | a little | a few; few; many + There were ___ people than last year. | fewer | less; few; lesser + Ik heb veel huiswerk: | I have a lot of homework. | I have many homeworks.; I have much homeworks.; I have a lot homeworks. + ` + }), + lesson({ + id: 'en-travel', + title: 'Travelling', + summary: 'Reizen, vervoer en het vliegveld', + notes: ` + Handige werkwoorden: + - **catch / take** a train — de trein nemen; **miss** — missen + - **get on / get off** — instappen / uitstappen (bus, trein, vliegtuig); **get in / get out of** — (auto, taxi) + - **check in** — inchecken; **board** — aan boord gaan + - **book** — boeken, reserveren + + **Travel** is een werkwoord of ontelbaar; een reis is **a trip** (kort) of **a journey** (de tocht zelf). + ~~a travel~~ → *a trip*. + `, + words: ` + trip | reis; uitstapje + journey | reis; tocht + ticket | kaartje + return ticket | retourtje + single ticket | enkeltje + platform | perron + delay | vertraging + luggage | bagage + suitcase | koffer + passport | paspoort + airport | vliegveld + flight | vlucht + get on | instappen + get off | uitstappen + miss | missen + book | boeken + abroad | in het buitenland + `, + sentences: ` + A return ticket to Brighton, please. | Een retourtje Brighton, alstublieft. + Which platform does the train leave from? | Van welk perron vertrekt de trein? + The flight is delayed by two hours. | De vlucht heeft twee uur vertraging. + We missed the last bus. | We hebben de laatste bus gemist. + Get off at the next stop. | Stap uit bij de volgende halte. + How was your trip? | Hoe was je reis? + We're going abroad this summer. | We gaan deze zomer naar het buitenland. + `, + drills: ` + Hoe was je reis? | How was your trip? | How was your travel?; How was your journey travel?; How was you trip? + ___ the bus at the station. (uitstappen) | Get off | Get out; Go off; Step off of + I ___ the train! (missed) | missed | lost; mist; failed + Een enkeltje: | a single ticket | a one ticket; an alone ticket; a single travel + ` + }) + ] +); diff --git a/src/lib/language-learner/courses/en/b1.ts b/src/lib/language-learner/courses/en/b1.ts new file mode 100644 index 0000000..bdb68e8 --- /dev/null +++ b/src/lib/language-learner/courses/en/b1.ts @@ -0,0 +1,401 @@ +import { lesson, level } from '../../course'; + +export default level( + 'b1', + 'B1', + 'Intermediate', + 'Present perfect, phrasal verbs, conditionals, passief en indirecte rede.', + [ + lesson({ + id: 'en-present-perfect', + title: 'I have seen it', + summary: 'Present perfect versus past simple (dé Nederlandse valkuil)', + notes: ` + Nederlands zegt "ik **heb** hem gisteren **gezien**", maar het Engels gebruikt dan de **past simple**: *I **saw** him yesterday*. + + | present perfect (have + voltooid deelwoord) | past simple | + |---|---| + | **geen** afgelopen tijdstip | **wel** een afgelopen tijdstip | + | ervaring: *I've been to Japan.* | *I went to Japan **in 2019**.* | + | resultaat nu: *I've lost my keys.* | *I lost my keys **yesterday**.* | + | tot nu toe: *I've lived here **for** ten years.* | *I lived there **for** ten years* (woon er niet meer) | + | ever, never, already, yet, just, so far | yesterday, ago, last …, in 2019, when … | + + ### for / since + *I've lived here **for** five years* (duur) / ***since** 2021* (beginpunt). + Nederlands "ik woon hier **al vijf jaar**" is dus ***I've lived / I've been living** here for five years* — niet ~~I live here since five years~~! + `, + words: ` + ever | ooit + never | nooit + already | al + yet | al (vraag); nog (niet) + just | net; zojuist + so far | tot nu toe + for | (al) … lang + since | sinds + been | geweest + seen | gezien + done | gedaan + lost | verloren + finished | af; klaar + `, + sentences: ` + Have you ever been to Canada? | Ben je ooit in Canada geweest? + I've never eaten sushi. | Ik heb nog nooit sushi gegeten. + I saw him yesterday. | Ik heb hem gisteren gezien. + She's just left. | Ze is net weggegaan. + Have you finished yet? | Ben je al klaar? + I've lived here for ten years. | Ik woon hier al tien jaar. + We've known each other since 2015. | We kennen elkaar sinds 2015. + I've lost my phone. | Ik ben mijn telefoon kwijt. + `, + drills: ` + Ik heb hem gisteren gezien: | I saw him yesterday. | I have seen him yesterday.; I've saw him yesterday.; I have see him yesterday. | Een afgelopen tijdstip → past simple. + Ik woon hier al drie jaar: | I've lived here for three years. | I live here since three years.; I live here for three years.; I've lived here since three years. + I've worked here ___ 2020. | since | for; from; ago + I've worked here ___ six months. | for | since; from; ago + ___ you ever ___ to Italy? | Have … been | Did … go; Have … go; Were … been + She hasn't called ___. | yet | already; still; ever + ` + }), + lesson({ + id: 'en-phrasal-1', + title: 'Get up, give up', + summary: 'Veelgebruikte phrasal verbs', + notes: ` + Een **phrasal verb** is werkwoord + partikel met een nieuwe betekenis. Net als Nederlandse scheidbare werkwoorden (opgeven, uitzoeken)! + + | phrasal verb | betekenis | + |---|---| + | get up | opstaan | + | give up | opgeven | + | look for | zoeken naar | + | look after | zorgen voor | + | find out | ontdekken, uitzoeken | + | turn on / off | aanzetten / uitzetten | + | put on / take off | aantrekken / uittrekken | + | pick up | oppakken, ophalen | + | call back | terugbellen | + | run out of | door … heen raken | + | come back | terugkomen | + | go out | uitgaan | + + Met een voornaamwoord staat dat **tussen** werkwoord en partikel: *Turn **it** off* (niet ~~Turn off it~~). + `, + words: ` + get up | opstaan + give up | opgeven + look for | zoeken naar + look after | zorgen voor + find out | ontdekken; uitzoeken + turn on | aanzetten + turn off | uitzetten + put on | aantrekken + take off | uittrekken; opstijgen + pick up | oppakken; ophalen + call back | terugbellen + run out of | door … heen raken + go out | uitgaan + come back | terugkomen + `, + sentences: ` + I get up at seven every day. | Ik sta elke dag om zeven uur op. + Don't give up! | Geef niet op! + Can you look after my cat? | Kun jij voor mijn kat zorgen? + Please turn off your phone. | Zet alsjeblieft je telefoon uit. + Put your coat on, it's cold. | Trek je jas aan, het is koud. + I'll pick you up at eight. | Ik haal je om acht uur op. + We've run out of milk. | De melk is op. + I'll call you back later. | Ik bel je later terug. + `, + drills: ` + Zet de tv uit: | Turn the TV off. | Turn off it.; Turn out the TV.; Make the TV off. + Zet hem uit: | Turn it off. | Turn off it.; Turn it out.; Off turn it. + Ik haal je op: | I'll pick you up. | I'll pick up you.; I'll get you up.; I'll take you up. + I'm looking ___ my keys. (zoeken) | for | after; out; up + She looks ___ her little brother. (zorgen voor) | after | for; at; out + We ran ___ of petrol. | out | off; over; away + ` + }), + lesson({ + id: 'en-conditionals', + title: 'If it rains…', + summary: 'First en second conditional', + notes: ` + ### First conditional — echt mogelijk + **If + present simple, will + werkwoord** + *If it **rains**, I**'ll stay** home.* (niet ~~If it will rain~~!) + + ### Second conditional — onwaarschijnlijk of denkbeeldig + **If + past simple, would + werkwoord** + *If I **had** more money, I**'d travel** the world.* + *If I **were** you, I**'d** say sorry.* (bij *I/he/she* mag *were*, en in "if I were you" is het de norm) + + Ook **unless** (tenzij) = if … not: *I won't go **unless** you come.* + **When** voor iets dat zeker gebeurt: ***When** I get home, I'll call you.* + `, + words: ` + if | als; indien + unless | tenzij + would | zou + when | wanneer; als + as soon as | zodra + in case | voor het geval dat + rich | rijk + lottery | loterij + win | winnen + miss | missen + `, + sentences: ` + If it rains, we'll stay inside. | Als het regent, blijven we binnen. + If I had more time, I'd learn to play the piano. | Als ik meer tijd had, zou ik piano leren spelen. + If I were you, I'd take the job. | Als ik jou was, zou ik de baan aannemen. + What would you do if you won the lottery? | Wat zou je doen als je de loterij won? + I'll call you when I arrive. | Ik bel je als ik aankom. + You'll miss the train unless you hurry. | Je mist de trein, tenzij je opschiet. + Take an umbrella in case it rains. | Neem een paraplu mee voor het geval het regent. + `, + drills: ` + If it ___ sunny tomorrow, we'll go to the beach. | is | will be; would be; was | Na if geen will. + If I ___ rich, I would buy a boat. | were | am; will be; would be + If I had time, I ___ help you. | would | will; did; had + Als ik jou was…: | If I were you… | If I would be you…; If I am you…; When I were you… + I won't come ___ you invite me. (tenzij) | unless | if; except; when + ` + }), + lesson({ + id: 'en-passive', + title: 'It was built in 1900', + summary: 'De lijdende vorm', + notes: ` + **Passive** = vorm van **be** + **voltooid deelwoord**. Het Engels gebruikt altijd *be* (Nederlands: *worden* én *zijn*). + + | tijd | voorbeeld | + |---|---| + | present simple | English **is spoken** here. | + | present continuous | The road **is being repaired**. | + | past simple | The house **was built** in 1900. | + | present perfect | My bike **has been stolen**. | + | will | It **will be finished** tomorrow. | + | modaal | It **can be done**. | + + De uitvoerder krijgt **by**: *Hamlet was written **by** Shakespeare.* + + Nederlanders zeggen vaak ~~I am born in 1995~~; het is ***I was born** in 1995*. + `, + words: ` + built | gebouwd + written | geschreven + made | gemaakt + stolen | gestolen + sold | verkocht + spoken | gesproken + invented | uitgevonden + born | geboren + by | door + repair | repareren + deliver | bezorgen + `, + sentences: ` + English is spoken all over the world. | Engels wordt over de hele wereld gesproken. + This bridge was built in 1890. | Deze brug is in 1890 gebouwd. + My bike has been stolen. | Mijn fiets is gestolen. + The package will be delivered tomorrow. | Het pakket wordt morgen bezorgd. + I was born in Rotterdam. | Ik ben in Rotterdam geboren. + The telephone was invented by Bell. | De telefoon is uitgevonden door Bell. + The road is being repaired. | De weg wordt gerepareerd. + `, + drills: ` + Ik ben in 1999 geboren: | I was born in 1999. | I am born in 1999.; I have been born in 1999.; I was borned in 1999. + The book ___ by Orwell. | was written | is writing; was wrote; has written + Mijn telefoon is gestolen: | My phone has been stolen. | My phone is stolen from me.; My phone has stolen.; My phone was being steal. + The room ___ every day. (cleaned) | is cleaned | cleans; is cleaning; is clean + It will ___ tomorrow. | be finished | finished; being finished; been finished + ` + }), + lesson({ + id: 'en-relative', + title: 'Who, which, that', + summary: 'Betrekkelijke bijzinnen', + notes: ` + | | gebruik | + |---|---| + | **who** | mensen: *the man **who** lives next door* | + | **which** | dingen, dieren: *the book **which** I bought* | + | **that** | mensen én dingen (alleen in bepalende bijzinnen) | + | **whose** | van wie / waarvan: *the girl **whose** dad is a pilot* | + | **where** | waar: *the town **where** I grew up* | + + Als het betrekkelijk voornaamwoord het **lijdend voorwerp** is, mag je het weglaten: *the book (that) I bought*. + + Met komma's (extra informatie, niet-bepalend) **nooit** *that*: *My brother, **who** lives in Spain, is a chef.* + `, + words: ` + who | die (mensen) + which | die; dat (dingen) + that | die; dat + whose | wiens; van wie + where | waar + neighbour | buurman; buurvrouw + grow up | opgroeien + recommend | aanraden + `, + sentences: ` + The woman who lives next door is a doctor. | De vrouw die hiernaast woont, is arts. + This is the book that I told you about. | Dit is het boek waarover ik je vertelde. + That's the restaurant where we met. | Dat is het restaurant waar we elkaar ontmoetten. + I have a friend whose mother is Korean. | Ik heb een vriend wiens moeder Koreaans is. + The film we watched was boring. | De film die we keken was saai. + My sister, who lives in Berlin, is visiting us. | Mijn zus, die in Berlijn woont, komt bij ons op bezoek. + `, + drills: ` + the man ___ helped me | who | which; whose; where + the car ___ I bought | which | who; whose; where + the town ___ I was born | where | which; who; that where + a girl ___ father is famous | whose | who's; who; which + My mum, ___ is 60, loves hiking. | who | that; which; whose | Met komma's geen that. + ` + }), + lesson({ + id: 'en-used-to', + title: 'I used to…', + summary: 'Used to, be used to en get used to', + notes: ` + Drie vormen die op elkaar lijken: + + | | betekenis | voorbeeld | + |---|---|---| + | **used to** + werkwoord | vroeger (nu niet meer) | *I **used to smoke**.* — Ik rookte vroeger. | + | **be used to** + -ing / zelfst. nw. | gewend zijn aan | *I'**m used to getting** up early.* | + | **get used to** + -ing / zelfst. nw. | wennen aan | *You'll **get used to** the weather.* | + + Vraag en ontkenning van *used to*: *Did you **use to** …? I **didn't use to** …* (zonder d). + `, + words: ` + used to | vroeger (deed ik) + be used to | gewend zijn aan + get used to | wennen aan + anymore | niet meer + childhood | jeugd + habit | gewoonte + smoke | roken + early | vroeg + noise | lawaai + `, + sentences: ` + I used to live in Amsterdam. | Ik woonde vroeger in Amsterdam. + She didn't use to like coffee. | Vroeger hield ze niet van koffie. + I'm used to working late. | Ik ben gewend om laat te werken. + You'll get used to the noise. | Je went wel aan het lawaai. + Did you use to play football? | Speelde jij vroeger voetbal? + I don't smoke anymore. | Ik rook niet meer. + `, + drills: ` + Ik speelde vroeger piano: | I used to play the piano. | I use to play the piano.; I was used to play the piano.; I used to playing the piano. + Ik ben gewend aan vroeg opstaan: | I'm used to getting up early. | I used to get up early.; I'm used to get up early.; I'm use to getting up early. + Did you ___ live here? | use to | used to; using to; used + It takes time to get used to ___ on the left. | driving | drive; drove; driven + ` + }), + lesson({ + id: 'en-gerund', + title: 'I enjoy swimming', + summary: 'Gerund (-ing) of infinitief (to)?', + notes: ` + Sommige werkwoorden worden gevolgd door **-ing**, andere door **to + werkwoord**. + + | + -ing | + to | + |---|---| + | enjoy, finish, mind, avoid, suggest, keep, practise, miss, can't stand | want, decide, hope, plan, promise, refuse, learn, need, would like, afford | + + - *I enjoy **swimming**.* (niet ~~enjoy to swim~~) + - *I decided **to leave**.* + + Na een **voorzetsel** altijd **-ing**: *I'm good **at cooking**. Thanks **for helping**. I look forward **to seeing** you.* + + Betekenisverschil: *I **stopped smoking*** (ik rook niet meer) vs. *I **stopped to smoke*** (ik stopte om te roken). + *Remember **to lock** the door* (vergeet het niet) vs. *I remember **locking** it* (ik weet nog dat ik het deed). + `, + words: ` + enjoy | genieten van + finish | afmaken + avoid | vermijden + suggest | voorstellen + mind | het erg vinden + decide | besluiten + hope | hopen + refuse | weigeren + afford | zich kunnen veroorloven + look forward to | uitkijken naar + be good at | goed zijn in + can't stand | niet kunnen uitstaan + `, + sentences: ` + I enjoy reading in bed. | Ik lees graag in bed. + She decided to quit her job. | Ze besloot haar baan op te zeggen. + Do you mind closing the window? | Vind je het erg om het raam dicht te doen? + I'm looking forward to seeing you. | Ik kijk ernaar uit je te zien. + He's really good at drawing. | Hij is echt goed in tekenen. + We can't afford to buy a house. | We kunnen ons geen huis veroorloven. + I can't stand waiting. | Ik kan niet tegen wachten. + `, + drills: ` + I enjoy ___. (cook) | cooking | to cook; cook; cooked + She wants ___ a doctor. (become) | to become | becoming; become; to becoming + I look forward to ___ you. | seeing | see; to see; saw | To is hier een voorzetsel. + He refused ___. (pay) | to pay | paying; pay; to paying + Would you mind ___ the door? | opening | to open; open; opened + I stopped ___. (ik rook niet meer) | smoking | to smoke; smoke; smoked + ` + }), + lesson({ + id: 'en-reported', + title: 'She said that…', + summary: 'Indirecte rede', + notes: ` + Na **said / told** schuift de tijd meestal een stap terug: + + | direct | indirect | + |---|---| + | "I **am** tired." | She said (that) she **was** tired. | + | "I **work** here." | He said he **worked** there. | + | "I **will** help." | He said he **would** help. | + | "I **can** come." | She said she **could** come. | + | "I **saw** it." | He said he **had seen** it. | + + **say** vs **tell**: *tell* heeft een persoon nodig: *She **told me** that…* / *She **said** that…* (niet ~~said me~~). + + Vragen: *He asked **where I lived*** (geen vraagvolgorde!), *She asked **if/whether** I was coming*. + Opdrachten: *He told me **to wait***. + `, + words: ` + say | zeggen + said | zei + tell | vertellen; zeggen (tegen) + told | vertelde + ask | vragen + whether | of + explain | uitleggen + reply | antwoorden + mention | noemen + admit | toegeven + `, + sentences: ` + She said she was tired. | Ze zei dat ze moe was. + He told me he would call. | Hij zei tegen me dat hij zou bellen. + They asked where we lived. | Ze vroegen waar we woonden. + She asked if I wanted coffee. | Ze vroeg of ik koffie wilde. + My boss told me to wait. | Mijn baas zei dat ik moest wachten. + He admitted that he had lied. | Hij gaf toe dat hij had gelogen. + `, + drills: ` + She ___ me that she was busy. | told | said; say; talked + He ___ that he was busy. | said | told; told me to; says me + "I'm hungry." → She said she ___ hungry. | was | is; were; has been + Hij vroeg waar ik woonde: | He asked where I lived. | He asked where did I live.; He asked where do I live.; He asked me where lived I. + "Wait!" → He told me ___. | to wait | wait; waiting; that wait + ` + }) + ] +); diff --git a/src/lib/language-learner/courses/en/b2.ts b/src/lib/language-learner/courses/en/b2.ts new file mode 100644 index 0000000..a6fd302 --- /dev/null +++ b/src/lib/language-learner/courses/en/b2.ts @@ -0,0 +1,347 @@ +import { lesson, level } from '../../course'; + +export default level( + 'b2', + 'B2', + 'Upper intermediate', + 'Third conditional, modal perfects, wensen, verbindingswoorden, valse vrienden en idioom.', + [ + lesson({ + id: 'en-third-conditional', + title: 'If I had known…', + summary: 'Third conditional en mixed conditionals', + notes: ` + ### Third conditional — denkbeeldig verleden + **If + past perfect, would have + voltooid deelwoord** + *If I **had known**, I **would have come**.* — Als ik het had geweten, was ik gekomen. + + In spreektaal: *If I'd known, I'd have come.* (Let op: *'d* = *had* na *if*, *would* in de hoofdzin.) + + ### Mixed conditional — verleden oorzaak, gevolg nu + *If I **had taken** that job, I **would be** rich now.* + + Nooit *would* in de if-zin: ~~If I would have known~~. + `, + words: ` + would have | zou hebben + could have | had kunnen + might have | had misschien + if only | had ik maar + regret | spijt hebben van + otherwise | anders + chance | kans + accept | accepteren; aannemen + `, + sentences: ` + If I had known, I would have come. | Als ik het had geweten, was ik gekomen. + If we had left earlier, we wouldn't have missed the train. | Als we eerder waren vertrokken, hadden we de trein niet gemist. + She would have passed if she had studied. | Ze was geslaagd als ze had gestudeerd. + If I had accepted that job, I would live in London now. | Als ik die baan had aangenomen, zou ik nu in Londen wonen. + What would you have done? | Wat zou jij hebben gedaan? + `, + drills: ` + If I ___ the answer, I would have told you. | had known | would have known; knew; have known + If she had asked, I ___ her. | would have helped | would help; had helped; will have helped + Als ik het had geweten…: | If I had known… | If I would have known…; If I knew…; If I have known… + If I had studied medicine, I ___ a doctor now. | would be | would have been; will be; had been | Mixed: verleden oorzaak, gevolg nu. + ` + }), + lesson({ + id: 'en-modal-perfect', + title: 'You should have told me', + summary: 'Should have, must have, could have', + notes: ` + **Modaal werkwoord + have + voltooid deelwoord** gaat over het verleden: + + | | betekenis | voorbeeld | + |---|---|---| + | should have | had moeten (maar niet gedaan) | *You **should have told** me.* | + | shouldn't have | had niet moeten | *I **shouldn't have eaten** so much.* | + | must have | moet wel (zeker) | *She **must have forgotten**.* | + | can't have | kan niet (onmogelijk) | *He **can't have seen** it.* | + | could have | had gekund | *We **could have won**.* | + | might / may have | misschien | *They **might have left** already.* | + + Gesproken klinkt *should have* als "should've" — schrijf **nooit** ~~should of~~. + `, + words: ` + should have | had moeten + must have | moet wel hebben + can't have | kan niet hebben + could have | had kunnen + might have | zou kunnen hebben + forget | vergeten + notice | opmerken + mistake | fout + apologise | zich verontschuldigen + `, + sentences: ` + You should have told me earlier. | Je had het me eerder moeten vertellen. + I shouldn't have said that. | Dat had ik niet moeten zeggen. + She must have missed the bus. | Ze heeft vast de bus gemist. + He can't have finished already. | Hij kan nog niet klaar zijn. + We could have won the match. | We hadden de wedstrijd kunnen winnen. + They might have got lost. | Misschien zijn ze verdwaald. + `, + drills: ` + Je had het moeten zeggen: | You should have said it. | You should said it.; You should of said it.; You must have said it. + The lights are off. They ___ gone out. (zeker) | must have | should have; can't have; must + He ___ stolen it; he was with me all day. | can't have | must have; shouldn't have; mustn't have + I'm so full. I ___ eaten that cake. | shouldn't have | mustn't have; can't have; shouldn't + ` + }), + lesson({ + id: 'en-phrasal-2', + title: 'Put up with it', + summary: 'Gevorderde phrasal verbs', + notes: ` + | phrasal verb | betekenis | + |---|---| + | put up with | tolereren, pikken | + | come up with | bedenken | + | look forward to | uitkijken naar | + | get along with | goed kunnen opschieten met | + | run into | tegen het lijf lopen | + | turn down | afwijzen; zachter zetten | + | carry on | doorgaan | + | figure out | uitvogelen | + | bring up | ter sprake brengen; opvoeden | + | put off | uitstellen | + | set up | opzetten, oprichten | + | break down | kapotgaan; instorten | + | work out | sporten; uitwerken, goed aflopen | + | call off | afgelasten | + | look into | onderzoeken | + `, + words: ` + put up with | tolereren; verdragen + come up with | bedenken + get along with | goed kunnen opschieten met + run into | tegen het lijf lopen + turn down | afwijzen; zachter zetten + carry on | doorgaan + figure out | uitvogelen + bring up | ter sprake brengen; opvoeden + put off | uitstellen + set up | opzetten; oprichten + break down | kapotgaan + work out | sporten; goed aflopen + call off | afgelasten + look into | onderzoeken + `, + sentences: ` + I can't put up with this noise any longer. | Ik kan dit lawaai niet langer verdragen. + She came up with a brilliant idea. | Ze bedacht een briljant idee. + Do you get along with your colleagues? | Kun je goed opschieten met je collega's? + I ran into an old friend yesterday. | Ik kwam gisteren een oude vriend tegen. + They turned down my offer. | Ze wezen mijn aanbod af. + Stop putting it off and just do it. | Stop met uitstellen en doe het gewoon. + The match was called off because of the rain. | De wedstrijd werd afgelast vanwege de regen. + Don't worry, it'll all work out. | Maak je geen zorgen, het komt allemaal goed. + `, + drills: ` + bedenken: come up ___ | with | to; for; on + The meeting was called ___. (afgelast) | off | out; down; away + I can't put up ___ him anymore. | with | on; for; to + Stop putting ___ your homework! (uitstellen) | off | away; down; out + We'll look ___ the problem. (onderzoeken) | into | after; for; out + My car broke ___ on the motorway. | down | up; off; out + ` + }), + lesson({ + id: 'en-wish', + title: 'I wish I could', + summary: 'Wish en if only', + notes: ` + | wens over | vorm | voorbeeld | + |---|---|---| + | het heden | wish + past simple | *I wish I **had** more time.* — Had ik maar meer tijd. | + | het verleden (spijt) | wish + past perfect | *I wish I **had studied** harder.* | + | irritatie over een ander | wish + would | *I wish you **would stop** talking.* | + | een vaardigheid | wish + could | *I wish I **could** sing.* | + + **If only** is sterker: *If only I **were** taller!* + Niet: ~~I wish I have~~ of ~~I wish I would have~~. + `, + words: ` + wish | wensen + if only | had … maar; als … maar + regret | spijt + instead | in plaats daarvan + complain | klagen + patient | geduldig + `, + sentences: ` + I wish I had more free time. | Had ik maar meer vrije tijd. + I wish I hadn't said that. | Had ik dat maar niet gezegd. + I wish you would listen to me. | Ik wou dat je naar me luisterde. + I wish I could speak Korean. | Kon ik maar Koreaans spreken. + If only I had known! | Had ik het maar geweten! + She wishes she lived by the sea. | Ze zou willen dat ze aan zee woonde. + `, + drills: ` + I wish I ___ taller. | were | am; will be; would be + I wish I ___ that film. (spijt, gezien) | hadn't watched | didn't watch; haven't watched; wouldn't watch + I wish it ___ raining! (irritatie) | would stop | stops; stopped to; will stop + Had ik maar meer geld: | I wish I had more money. | I wish I have more money.; I wish I would have more money.; I wish I had had more money now. + ` + }), + lesson({ + id: 'en-linking', + title: 'However, although, despite', + summary: 'Verbindingswoorden', + notes: ` + | | + | voorbeeld | + |---|---|---| + | although / even though | een bijzin | *Although it rained, we went out.* | + | despite / in spite of | zelfstandig naamwoord of -ing | *Despite the rain, we went out.* | + | however | nieuwe zin, met komma | *It rained. However, we went out.* | + | whereas / while | contrast tussen twee dingen | *I like tea, whereas she prefers coffee.* | + | therefore / as a result | gevolg | *It rained. Therefore, the match was cancelled.* | + | moreover / furthermore / in addition | toevoeging (formeel) | | + | due to / because of | oorzaak + zelfst. nw. | *due to the weather* | + + Fout: ~~Despite it rained~~ → *Despite **the rain*** of *Despite **the fact that** it rained*. + `, + words: ` + although | hoewel + even though | ook al + despite | ondanks + in spite of | ondanks + however | echter + whereas | terwijl (contrast) + therefore | daarom + moreover | bovendien + furthermore | verder; bovendien + due to | vanwege; als gevolg van + on the other hand | aan de andere kant + nevertheless | niettemin + `, + sentences: ` + Although it was cold, we went swimming. | Hoewel het koud was, gingen we zwemmen. + Despite the rain, the concert went ahead. | Ondanks de regen ging het concert door. + The hotel was nice. However, it was expensive. | Het hotel was mooi. Het was echter duur. + I love cities, whereas my partner prefers the countryside. | Ik hou van steden, terwijl mijn partner het platteland verkiest. + The flight was cancelled due to fog. | De vlucht werd geannuleerd vanwege mist. + He was tired; nevertheless, he kept working. | Hij was moe; toch bleef hij doorwerken. + `, + drills: ` + ___ the traffic, we arrived on time. | Despite | Although; However; Even though + ___ it was late, she kept working. | Although | Despite; However; In spite of + It was cheap. ___, it broke after a week. | However | Although; Despite; Whereas + ondanks dat het regende: | despite the fact that it rained | despite it rained; despite of the rain; in spite it rained + The game was cancelled ___ the storm. | due to | because; although; despite + ` + }), + lesson({ + id: 'en-false-friends', + title: 'False friends', + summary: 'Valse vrienden tussen Nederlands en Engels', + notes: ` + | Engels | betekent | Nederlands woord dat erop lijkt | = in het Engels | + |---|---|---|---| + | eventually | uiteindelijk | eventueel | possibly, if necessary | + | actually | eigenlijk, in feite | actueel | current, topical | + | sympathetic | meelevend | sympathiek | likeable, nice | + | brave | dapper | braaf | good, well-behaved | + | become | worden | bekomen | recover | + | control | beheersen | controleren | check | + | familiar | bekend, vertrouwd | familie | family, relatives | + | sensible | verstandig | sensibel | sensitive | + | chef | kok | chef | boss, manager | + | genial | vriendelijk, joviaal | geniaal | brilliant | + | undertaker | begrafenisondernemer | ondernemer | entrepreneur | + | fabric | stof (textiel) | fabriek | factory | + | customer | klant | kostuum | costume, suit | + | kind | aardig | kind | child | + `, + words: ` + eventually | uiteindelijk + possibly | eventueel; mogelijk + actually | eigenlijk + current | actueel; huidig + sympathetic | meelevend + likeable | sympathiek + brave | dapper + well-behaved | braaf + check | controleren + familiar | vertrouwd; bekend + relatives | familie(leden) + sensible | verstandig + sensitive | gevoelig; sensibel + boss | chef; baas + factory | fabriek + brilliant | geniaal + `, + sentences: ` + Eventually, we found the hotel. | Uiteindelijk vonden we het hotel. + We could possibly meet tomorrow. | We kunnen eventueel morgen afspreken. + Actually, I don't like coffee. | Eigenlijk hou ik niet van koffie. + Can you check the numbers? | Kun jij de cijfers controleren? + He's a very likeable guy. | Hij is een heel sympathieke gozer. + That's a very sensible decision. | Dat is een heel verstandig besluit. + She was very brave. | Ze was heel dapper. + I'm visiting relatives this weekend. | Ik ga dit weekend bij familie op bezoek. + `, + drills: ` + eventueel: | possibly | eventually; actual; eventual + uiteindelijk: | eventually | possibly; actually; actual + controleren: | check | control; controle; test out + Hij is heel sympathiek: | He's very likeable. | He's very sympathetic.; He's very sympathic.; He's very brave. + een verstandige keuze: | a sensible choice | a sensitive choice; a sensibel choice; a sense choice + Ik ga naar mijn familie: | I'm visiting relatives. | I'm visiting my familiars.; I'm visiting my family's.; I'm visiting familiar. + ` + }), + lesson({ + id: 'en-idioms-1', + title: 'Piece of cake', + summary: 'Veelgebruikte Engelse uitdrukkingen', + notes: ` + | idioom | betekenis | Nederlands | + |---|---|---| + | a piece of cake | heel makkelijk | een fluitje van een cent | + | it's raining cats and dogs | het giet | het regent pijpenstelen | + | break the ice | de spanning breken | het ijs breken | + | under the weather | niet lekker | niet lekker in je vel | + | hit the nail on the head | precies goed | de spijker op zijn kop slaan | + | cost an arm and a leg | peperduur | een rib uit je lijf | + | once in a blue moon | zelden | eens in de zoveel tijd | + | let the cat out of the bag | een geheim verklappen | je mond voorbij praten | + | the ball is in your court | jij bent aan zet | de bal ligt bij jou | + | call it a day | ermee stoppen voor vandaag | het voor gezien houden | + | pull someone's leg | iemand plagen | iemand in de maling nemen | + | on the fence | twijfelen | op twee gedachten hinken | + `, + words: ` + a piece of cake | een fluitje van een cent + raining cats and dogs | regent pijpenstelen + break the ice | het ijs breken + under the weather | niet lekker; grieperig + hit the nail on the head | de spijker op zijn kop slaan + cost an arm and a leg | een rib uit je lijf kosten + once in a blue moon | zelden + let the cat out of the bag | een geheim verklappen + call it a day | ermee ophouden (voor vandaag) + pull someone's leg | iemand in de maling nemen + on the fence | twijfelen + the ball is in your court | jij bent aan zet + `, + sentences: ` + The exam was a piece of cake. | Het examen was een fluitje van een cent. + I'm feeling a bit under the weather. | Ik voel me een beetje niet lekker. + That car must have cost an arm and a leg. | Die auto moet een rib uit je lijf hebben gekost. + I only see him once in a blue moon. | Ik zie hem maar zelden. + It's late, let's call it a day. | Het is laat, laten we ermee stoppen. + Are you pulling my leg? | Neem je me in de maling? + You hit the nail on the head. | Je slaat de spijker op zijn kop. + `, + drills: ` + Heel makkelijk: a piece of ___ | cake | pie; bread; biscuit + Het regent pijpenstelen: it's raining ___ | cats and dogs | pipes and stems; buckets and dogs; cows and horses + Peperduur: it cost an arm and a ___ | leg | foot; hand; head + Zelden: once in a ___ moon | blue | full; red; silver + Een geheim verklappen: let the ___ out of the bag | cat | dog; cake; secret + ` + }) + ] +); diff --git a/src/lib/language-learner/courses/en/c1.ts b/src/lib/language-learner/courses/en/c1.ts new file mode 100644 index 0000000..8374427 --- /dev/null +++ b/src/lib/language-learner/courses/en/c1.ts @@ -0,0 +1,318 @@ +import { lesson, level } from '../../course'; + +export default level( + 'c1', + 'C1', + 'Advanced', + 'Inversie, cleft sentences, formeel register, collocaties en lastige woordparen.', + [ + lesson({ + id: 'en-inversion', + title: 'Never have I…', + summary: 'Inversie voor nadruk', + notes: ` + Na een **ontkennende of beperkende** uitdrukking aan het begin van de zin draait de volgorde om, zoals in een vraag: + + | begin | voorbeeld | + |---|---| + | Never | ***Never have I seen** such chaos.* | + | Rarely / Seldom | ***Rarely does he** complain.* | + | Not only | ***Not only did she** win, but she also broke the record.* | + | Hardly … when | ***Hardly had we** arrived when it started to rain.* | + | No sooner … than | ***No sooner had I** sat down than the phone rang.* | + | Under no circumstances | ***Under no circumstances should you** open this.* | + | Only then / Only when | ***Only then did I** realise my mistake.* | + | Little | ***Little did they** know…* | + + Conditionals zonder *if*: ***Had I** known… = If I had known… / **Should you** need help… / **Were I** you…* + `, + words: ` + never have I | nooit heb ik + rarely | zelden + seldom | zelden + not only … but also | niet alleen … maar ook + hardly … when | nauwelijks … of + no sooner … than | nauwelijks … of + under no circumstances | onder geen enkele voorwaarde + only then | pas toen + little did I know | ik had geen idee + should you | mocht je + `, + sentences: ` + Never have I seen such a beautiful sunset. | Nog nooit heb ik zo'n mooie zonsondergang gezien. + Not only is he rude, but he's also lazy. | Hij is niet alleen onbeleefd, maar ook lui. + No sooner had I left than it started raining. | Ik was nog niet weg of het begon te regenen. + Under no circumstances should you open the door. | Onder geen beding mag je de deur openen. + Little did I know what was about to happen. | Ik had geen idee wat er ging gebeuren. + Should you need any help, please let me know. | Mocht je hulp nodig hebben, laat het me dan weten. + Had I known, I would have called. | Had ik het geweten, dan had ik gebeld. + `, + drills: ` + Never ___ such a mess. | have I seen | I have seen; I saw; seen I have + Rarely ___ late. | does she arrive | she arrives; arrives she; she does arrive + Not only ___ the exam, but she also got top marks. | did she pass | she passed; passed she; she did pass + ___ you have any questions, call me. (Mocht) | Should | Would; If should; Shall + ___ I known, I would have helped. | Had | If; Have; Would + ` + }), + lesson({ + id: 'en-cleft', + title: 'What I need is…', + summary: 'Cleft sentences voor nadruk', + notes: ` + Een **cleft sentence** splitst een zin om één deel te benadrukken: + + - **It**-cleft: ***It was** Tom **who** broke it.* (niet iemand anders) + - **What**-cleft: ***What I need is** a holiday.* + - **All**-cleft: ***All I want is** some peace.* — Het enige wat ik wil… + - **The reason why**: ***The reason why** I left **is that** …* + - **The thing is**: ***The thing is**, I'm broke.* + + Handig in het Engels, omdat je (anders dan in het Nederlands) de woordvolgorde niet vrij kunt omgooien voor nadruk. + `, + words: ` + it was … who | het was … die + what I need is | wat ik nodig heb is + all I want is | het enige wat ik wil is + the reason why | de reden waarom + the thing is | het punt is + the last thing | het laatste + what happened was | wat er gebeurde was + `, + sentences: ` + It was Sarah who called, not Emma. | Het was Sarah die belde, niet Emma. + What I really need is a long holiday. | Wat ik echt nodig heb is een lange vakantie. + All I want is a cup of tea. | Het enige wat ik wil is een kopje thee. + The reason why I'm late is that the train was cancelled. | De reden dat ik te laat ben, is dat de trein uitviel. + The thing is, I don't have enough money. | Het punt is, ik heb niet genoeg geld. + It wasn't until midnight that he arrived. | Hij kwam pas om middernacht aan. + `, + drills: ` + ___ I need is some sleep. | What | That; Which; It + It was my brother ___ told me. | who | which; what; whom he + ___ I want is to go home. (Het enige) | All | What only; Only; The all + It wasn't until Friday ___ I heard the news. | that | when; than; which + ` + }), + lesson({ + id: 'en-formal', + title: 'Formal register', + summary: 'Formeel en academisch Engels', + notes: ` + | informeel | formeel | + |---|---| + | get | obtain, receive | + | ask for | request | + | find out | discover, ascertain | + | help | assist | + | need | require | + | start | commence | + | end | terminate, conclude | + | enough | sufficient | + | buy | purchase | + | but | however | + | so | therefore, consequently | + | a lot of | a considerable amount of, numerous | + + Formeel schrijven vermijdt **afkortingen** (*don't → do not*), gebruikt vaker de **passief** en onpersoonlijke constructies: *It is recommended that…*, *It should be noted that…* + + Brieven: *Dear Sir or Madam, … Yours faithfully* (naam onbekend) / *Dear Ms Smith, … Yours sincerely* (naam bekend). + `, + words: ` + obtain | verkrijgen + request | verzoeken; verzoek + assist | assisteren; helpen + require | vereisen + commence | aanvangen + conclude | besluiten; afronden + sufficient | voldoende + purchase | aanschaffen; aankoop + consequently | bijgevolg + numerous | talrijk + regarding | betreffende + hereby | hierbij + enclosed | bijgevoegd + yours faithfully | hoogachtend + `, + sentences: ` + I am writing to request further information. | Ik schrijf u om nadere informatie te vragen. + Please find enclosed a copy of my CV. | Bijgevoegd vindt u een kopie van mijn cv. + Applicants are required to have a valid passport. | Kandidaten moeten in het bezit zijn van een geldig paspoort. + It should be noted that the results are preliminary. | Er moet worden opgemerkt dat de resultaten voorlopig zijn. + Consequently, the project was postponed. | Bijgevolg werd het project uitgesteld. + I look forward to hearing from you. | Ik zie uw reactie met belangstelling tegemoet. + `, + drills: ` + Formal "buy": | purchase | get; shop; grab + Formal "help": | assist | help out; lend a hand; give a hand + Formal "enough": | sufficient | plenty; loads; enough of + You don't know the recipient's name. End with: | Yours faithfully | Yours sincerely; Kind regards; Cheers + Formal "start": | commence | kick off; get going; start up + ` + }), + lesson({ + id: 'en-collocations', + title: 'Make or do?', + summary: 'Collocaties: woorden die bij elkaar horen', + notes: ` + ### make vs do + | make | do | + |---|---| + | a mistake, a decision, a phone call | homework, the dishes, the shopping | + | money, progress, an effort, noise | business, a favour, your best | + | friends, a mess, sense | research, damage, exercise | + + ### Andere vaste combinaties + - **heavy** rain / traffic (niet ~~strong rain~~) + - **strong** coffee / wind (niet ~~heavy coffee~~) + - **take** a photo / a break / a decision (BrE ook: make) + - **pay** attention / a visit / a compliment + - **catch** a cold / a bus + - **keep** a promise / a secret + - **commit** a crime + - **raise** a question / children + `, + words: ` + make a mistake | een fout maken + make a decision | een besluit nemen + make an effort | moeite doen + do homework | huiswerk maken + do the dishes | afwassen + do someone a favour | iemand een plezier doen + heavy rain | zware regen + heavy traffic | druk verkeer + strong coffee | sterke koffie + take a break | pauze nemen + pay attention | opletten + catch a cold | verkouden worden + keep a promise | een belofte houden + commit a crime | een misdaad plegen + `, + sentences: ` + Everyone makes mistakes. | Iedereen maakt fouten. + Could you do me a favour? | Zou je me een plezier willen doen? + I have to do the shopping. | Ik moet boodschappen doen. + There was heavy traffic on the motorway. | Er was druk verkeer op de snelweg. + Let's take a short break. | Laten we een korte pauze nemen. + Please pay attention. | Let alsjeblieft op. + She always keeps her promises. | Ze houdt zich altijd aan haar beloftes. + `, + drills: ` + ___ a mistake | make | do; take; have + ___ your homework | do | make; take; have + ___ rain | heavy | strong; big; hard + ___ coffee | strong | heavy; hard; big + ___ someone a favour | do | make; give; take + ___ attention | pay | give; make; take + ___ progress | make | do; get; take + ` + }), + lesson({ + id: 'en-idioms-2', + title: 'Beat around the bush', + summary: 'Gevorderde uitdrukkingen', + notes: ` + | idioom | betekenis | + |---|---| + | beat around the bush | eromheen draaien | + | bite off more than you can chew | te veel hooi op je vork nemen | + | cut corners | de kantjes eraf lopen | + | get the ball rolling | de bal aan het rollen brengen | + | a blessing in disguise | een geluk bij een ongeluk | + | the elephant in the room | het probleem waar niemand over begint | + | jump on the bandwagon | op de trein springen, meedoen met een trend | + | miss the boat | de boot missen | + | sit on the fence | de kat uit de boom kijken | + | a storm in a teacup | een storm in een glas water | + | go the extra mile | een stapje extra zetten | + | spill the beans | een geheim verklappen | + `, + words: ` + beat around the bush | eromheen draaien + bite off more than you can chew | te veel hooi op je vork nemen + cut corners | de kantjes eraf lopen + get the ball rolling | de bal aan het rollen brengen + a blessing in disguise | een geluk bij een ongeluk + the elephant in the room | het heikele onderwerp dat iedereen negeert + jump on the bandwagon | met de massa meedoen + miss the boat | de boot missen + a storm in a teacup | een storm in een glas water + go the extra mile | een stapje extra zetten + spill the beans | alles verklappen + `, + sentences: ` + Stop beating around the bush and tell me. | Stop met eromheen draaien en vertel het me. + I think I've bitten off more than I can chew. | Ik denk dat ik te veel hooi op mijn vork heb genomen. + Losing that job was a blessing in disguise. | Die baan verliezen was een geluk bij een ongeluk. + It was just a storm in a teacup. | Het was gewoon een storm in een glas water. + She always goes the extra mile for her clients. | Ze zet altijd een stapje extra voor haar klanten. + Come on, spill the beans! | Kom op, vertel! + Let's get the ball rolling. | Laten we beginnen. + `, + drills: ` + Een storm in een glas water: a storm in a ___ | teacup | glass of water; bathtub; coffee cup + Te veel hooi op je vork: bite off more than you can ___ | chew | eat; swallow; carry + De kantjes eraf lopen: cut ___ | corners | edges; sides; the edges off + Eromheen draaien: beat around the ___ | bush | tree; house; hedge + Een geluk bij een ongeluk: a blessing in ___ | disguise | the dark; hiding; secret + ` + }), + lesson({ + id: 'en-tricky', + title: 'Affect or effect?', + summary: 'Woordparen die zelfs moedertaalsprekers verwarren', + notes: ` + | | | + |---|---| + | **affect** (ww.) — beïnvloeden | **effect** (znw.) — gevolg | + | **lie** (lay, lain) — liggen | **lay** (laid, laid) — leggen | + | **fewer** — telbaar | **less** — ontelbaar | + | **its** — zijn/haar | **it's** — it is | + | **their** — hun / **there** — daar | **they're** — they are | + | **then** — dan, toen | **than** — dan (vergelijking) | + | **lose** — verliezen | **loose** — los | + | **advice** (znw.) — advies | **advise** (ww.) — adviseren | + | **who** — onderwerp | **whom** — lijdend voorwerp (formeel) | + | **borrow** — lenen van | **lend** — lenen aan | + | **rise** — stijgen (zelf) | **raise** — verhogen (iets) | + + Typische Nederlandse fouten: ~~I am agree~~ → *I **agree***, ~~Can you lend me your pen?~~ is goed, ~~Can I lend your pen?~~ → *Can I **borrow** your pen?* + `, + words: ` + affect | beïnvloeden + effect | effect; gevolg + lie | liggen; liegen + lay | leggen + fewer | minder (telbaar) + less | minder (ontelbaar) + lose | verliezen + loose | los; wijd + advise | adviseren + borrow | lenen (van iemand) + lend | uitlenen (aan iemand) + rise | stijgen + raise | verhogen; opheffen + agree | het eens zijn + `, + sentences: ` + The weather affects my mood. | Het weer beïnvloedt mijn humeur. + The medicine had no effect. | Het medicijn had geen effect. + Can I borrow your pen? | Mag ik je pen lenen? + Could you lend me ten euros? | Kun je me tien euro lenen? + I agree with you. | Ik ben het met je eens. + Prices have risen sharply. | De prijzen zijn sterk gestegen. + They raised their prices. | Ze hebben hun prijzen verhoogd. + Don't lose your keys. | Raak je sleutels niet kwijt. + `, + drills: ` + Stress can ___ your health. | affect | effect; infect; afect + It had a big ___ on me. | effect | affect; affection; efect + Ik ben het met je eens: | I agree with you. | I am agree with you.; I agree to you.; I'm agreeing you. + Can I ___ your car? (lenen van jou) | borrow | lend; loan; lent + ___ people came than expected. | Fewer | Less; Lesser; Fewest + The dog wagged ___ tail. | its | it's; his; their + Prices ___ every year. | rise | raise; arise; rises + ` + }) + ] +); diff --git a/src/lib/language-learner/courses/en/index.ts b/src/lib/language-learner/courses/en/index.ts new file mode 100644 index 0000000..306a526 --- /dev/null +++ b/src/lib/language-learner/courses/en/index.ts @@ -0,0 +1,18 @@ +import { course } from '../../course'; +import a1 from './a1'; +import a2 from './a2'; +import b1 from './b1'; +import b2 from './b2'; +import c1 from './c1'; + +export default course( + { + code: 'en', + name: 'English', + nativeName: 'English', + glossLang: 'nl', + voice: 'en-GB', + tagline: 'Van de basis tot vloeiend Engels, uitgelegd in het Nederlands.' + }, + [a1, a2, b1, b2, c1] +); diff --git a/src/lib/language-learner/courses/index.ts b/src/lib/language-learner/courses/index.ts new file mode 100644 index 0000000..d2529f4 --- /dev/null +++ b/src/lib/language-learner/courses/index.ts @@ -0,0 +1,48 @@ +import type { Course, LangCode } from '../types'; + +export type CourseInfo = { + code: LangCode; + name: string; + nativeName: string; + tagline: string; + glossLang: LangCode; +}; + +/** Lightweight metadata; the full course is loaded on demand. */ +export const COURSES: CourseInfo[] = [ + { + code: 'nl', + name: 'Dutch', + nativeName: 'Nederlands', + tagline: 'From "hallo" to reading the news, taught in English.', + glossLang: 'en' + }, + { + code: 'ko', + name: 'Korean', + nativeName: '한국어', + tagline: 'Hangul first, then grammar and speech levels, taught in English.', + glossLang: 'en' + }, + { + code: 'en', + name: 'English', + nativeName: 'English', + tagline: 'Van de basis tot vloeiend Engels, uitgelegd in het Nederlands.', + glossLang: 'nl' + } +]; + +export const isLang = (value: string): value is LangCode => COURSES.some((c) => c.code === value); + +export const courseInfo = (code: LangCode) => COURSES.find((c) => c.code === code)!; + +const loaders: Record Promise<{ default: Course }>> = { + nl: () => import('./nl'), + ko: () => import('./ko'), + en: () => import('./en') +}; + +export async function loadCourse(code: LangCode): Promise { + return (await loaders[code]()).default; +} diff --git a/src/lib/language-learner/courses/ko/a1.ts b/src/lib/language-learner/courses/ko/a1.ts new file mode 100644 index 0000000..224c385 --- /dev/null +++ b/src/lib/language-learner/courses/ko/a1.ts @@ -0,0 +1,465 @@ +import { lesson, level } from '../../course'; + +export default level( + 'a1', + 'A1', + 'Beginner', + 'Greetings, introductions, numbers, particles and your first verbs.', + [ + lesson({ + id: 'ko-greetings', + title: '안녕하세요', + summary: 'Greetings, thanks and apologies', + notes: ` + Korean has **speech levels**. This course starts with the polite **해요체** (ending in **-요**), which is safe with almost anyone. + + **안녕하세요** literally asks "are you at peace?" and works any time of day. + + ### Two ways to say goodbye + - **안녕히 가세요** — "go in peace": say it to the person **leaving**. + - **안녕히 계세요** — "stay in peace": say it to the person **staying** (when you leave). + + Thanks: **감사합니다** (a bit more formal) and **고맙습니다** are both fine. **고마워요** is softer. + Sorry: **죄송합니다** (formal, serious), **미안해요** (polite, lighter). + `, + words: ` + 안녕하세요 | hello + 안녕히 가세요 | goodbye (to someone leaving) + 안녕히 계세요 | goodbye (to someone staying) + 감사합니다 | thank you + 고맙습니다 | thank you + 죄송합니다 | I'm sorry (formal) + 미안해요 | I'm sorry + 괜찮아요 | it's okay; I'm fine + 네; 예 | yes + 아니요 | no + 반갑습니다 | nice to meet you + 실례합니다 | excuse me + 여보세요 | hello (on the phone) + 잘 부탁드립니다 | please treat me well; I look forward to working with you + `, + sentences: ` + 안녕하세요, 반갑습니다. | Hello, nice to meet you. + 정말 감사합니다. | Thank you very much. + 괜찮아요? | Are you okay? + 네, 괜찮아요. | Yes, I'm fine. + 늦어서 죄송합니다. | I'm sorry for being late. + 잘 부탁드립니다. | I look forward to working with you. + `, + drills: ` + You are leaving a shop. You say to the shopkeeper: | 안녕히 계세요 | 안녕히 가세요; 안녕하세요; 여보세요 | They stay, so: stay in peace. + A guest leaves your home. You say: | 안녕히 가세요 | 안녕히 계세요; 반갑습니다; 실례합니다 + You pick up the phone: | 여보세요 | 안녕히 계세요; 실례합니다; 괜찮아요 + You bump into someone on the street: | 죄송합니다 | 감사합니다; 반갑습니다; 괜찮아요 + ` + }), + lesson({ + id: 'ko-introductions', + title: '저는 학생이에요', + summary: 'Introducing yourself with 이에요/예요', + notes: ` + ### "To be": 이에요 / 예요 + To say "X is Y", attach **이에요** after a consonant or **예요** after a vowel: + - 학생**이에요** — (I) am a student + - 의사**예요** — (I) am a doctor + + The negative is **아니에요**, with **이/가** on the noun: *학생**이** 아니에요* — (I) am not a student. + + ### 저 and 는 + **저** is the humble "I". **저는** = "as for me" (the **topic** particle 는). Korean often drops the subject when it's obvious. + + Korean word order is **subject – object – verb**: the verb is always last. + + Nationality: country + **사람**: 한국 사람, 네덜란드 사람, 영국 사람, 미국 사람. + `, + words: ` + 저 | I; me (humble) + 이름 | name + 학생 | student + 선생님 | teacher + 회사원 | office worker + 의사 | doctor + 사람 | person + 나라 | country + 네덜란드 | the Netherlands + 미국 | the USA + 영국 | the UK + 어느 | which + 뭐 | what + 아니에요 | is not + 이에요; 예요 | is; am; are + `, + sentences: ` + 저는 학생이에요. | I am a student. + 이름이 뭐예요? | What is your name? + 제 이름은 민수예요. | My name is Minsu. + 어느 나라 사람이에요? | Where are you from? (Which country's person?) + 저는 네덜란드 사람이에요. | I am Dutch. + 저는 의사가 아니에요. | I'm not a doctor. + 선생님이에요? | Are you a teacher? + `, + drills: ` + 저는 학생___. | 이에요 | 예요; 이다; 은요 | 학생 ends in a consonant. + 저는 의사___. | 예요 | 이에요; 이예요; 가요 | 의사 ends in a vowel. + 저는 선생님___ 아니에요. | 이 | 은; 을; 가 + 제 이름___ 지민이에요. | 은 | 는; 이; 을 | 이름 ends in a consonant: 은. + "Which country?" | 어느 나라 | 무슨 나라; 뭐 나라; 어디 나라 + ` + }), + lesson({ + id: 'ko-this-that', + title: '이게 뭐예요?', + summary: 'This, that, and the topic and subject particles', + notes: ` + ### 이, 그, 저 + | | near me | near you | over there | + |---|---|---|---| + | this/that + noun | 이 책 | 그 책 | 저 책 | + | thing | 이것 (이거) | 그것 (그거) | 저것 (저거) | + | place | 여기 | 거기 | 저기 | + + In speech: **이게** = 이것이, **그게** = 그것이, **뭐예요** = 무엇이에요. + + ### 은/는 (topic) vs 이/가 (subject) + - **은/는** marks the **topic**, often something already known or contrasted: *저는 학생이에요.* + - **이/가** marks the **subject**, often new information or an answer to "who/what": *누**가** 왔어요? 민수**가** 왔어요.* + + Form: **은 / 이** after a consonant, **는 / 가** after a vowel. + + **제** = my (humble, from 저의). *제 책* = my book. + `, + words: ` + 이것; 이거 | this (thing) + 그것; 그거 | that (thing, near you) + 저것; 저거 | that (thing, over there) + 여기 | here + 거기 | there (near you) + 저기 | over there + 누구 | who + 제 | my + 책 | book + 가방 | bag + 휴대폰; 핸드폰 | mobile phone + 시계 | watch; clock + 우산 | umbrella + 지갑 | wallet + 열쇠 | key + 친구 | friend + `, + sentences: ` + 이게 뭐예요? | What is this? + 그건 제 가방이에요. | That is my bag. + 저 사람은 누구예요? | Who is that person? + 제 친구예요. | It's my friend. + 여기가 어디예요? | Where is this place? + 이 우산은 누구 거예요? | Whose umbrella is this? + 그 책이 재미있어요. | That book is interesting. + `, + drills: ` + (near you) ___ 뭐예요? | 그게 | 이게; 저게; 거기 | 그 = near the listener. + 가방___ 어디에 있어요? | 이 | 가; 은; 을 + 친구___ 한국 사람이에요. (topic) | 는 | 은; 가; 이 + 누가 했어요? – 제 동생___ 했어요. | 이 | 은; 가; 는 | An answer to "who" takes 이/가. + ___ 책은 제 책이에요. (this) | 이 | 이거; 여기; 이게 + ` + }), + lesson({ + id: 'ko-sino-numbers', + title: '일, 이, 삼', + summary: 'Sino-Korean numbers: money, dates, phone numbers', + notes: ` + Korean has **two** number systems. **Sino-Korean** (from Chinese) is used for: + money, dates, phone numbers, minutes, floors, addresses, and anything above 99. + + | | | | | | + |---|---|---|---|---| + | 1 일 | 2 이 | 3 삼 | 4 사 | 5 오 | + | 6 육 | 7 칠 | 8 팔 | 9 구 | 10 십 | + + Bigger numbers are built like a calculator: **이십삼** = 2×10+3 = 23. **백** 100, **천** 1,000, **만** 10,000. + No 일 before 십, 백, 천, 만: 110 = **백십**, 10,000 = **만**. + + **만** trips up learners: 50,000 won is **오만 원** (5 × 10,000), 1,000,000 is **백만**. + + Zero is **영** or, in phone numbers, **공**: 010-1234-5678 = 공일공 일이삼사 오육칠팔. + + Dates: **년** (year), **월** (month), **일** (day): 2026년 3월 1일 = 이천이십육 년 삼월 일 일. June = **유월**, October = **시월**. + `, + words: ` + 일 | one (Sino) + 이 | two (Sino) + 삼 | three (Sino) + 사 | four (Sino) + 오 | five (Sino) + 육 | six (Sino) + 칠 | seven (Sino) + 팔 | eight (Sino) + 구 | nine (Sino) + 십 | ten (Sino) + 백 | hundred + 천 | thousand + 만 | ten thousand + 원 | won (currency) + 얼마 | how much + 전화번호 | phone number + 몇 | how many; what number + 생일 | birthday + `, + sentences: ` + 이거 얼마예요? | How much is this? + 만 원이에요. | It's 10,000 won. + 삼천오백 원이에요. | It's 3,500 won. + 전화번호가 뭐예요? | What's your phone number? + 생일이 몇 월 며칠이에요? | When is your birthday? + 제 생일은 유월 십일이에요. | My birthday is June 10th. + 몇 층이에요? | Which floor is it? + `, + drills: ` + 15 (Sino) | 십오 | 일십오; 오십; 열다섯 + 300 (Sino) | 삼백 | 삼십; 삼천; 셋백 + 10,000 (Sino) | 만 | 일만천; 십천; 천 + ₩50,000 | 오만 원 | 오천 원; 오십만 원; 오백 원 + June | 유월 | 육월; 여섯월; 유울 | 육 loses its ㄱ in 유월. + October | 시월 | 십월; 열월; 시울 + ` + }), + lesson({ + id: 'ko-native-numbers', + title: '하나, 둘, 셋', + summary: 'Native Korean numbers: counting, age, hours', + notes: ` + **Native Korean** numbers go from 1 to 99 and are used for **counting things**, **age** and **hours**. + + | | | | | | + |---|---|---|---|---| + | 1 하나 | 2 둘 | 3 셋 | 4 넷 | 5 다섯 | + | 6 여섯 | 7 일곱 | 8 여덟 | 9 아홉 | 10 열 | + + Tens: 20 스물, 30 서른, 40 마흔, 50 쉰, 60 예순, 70 일흔, 80 여든, 90 아흔. + + ### Counters + Korean counts with **number + counter**. Before a counter, 1–4 and 20 shorten: + **하나 → 한, 둘 → 두, 셋 → 세, 넷 → 네, 스물 → 스무**. + + | counter | for | example | + |---|---|---| + | 개 | things | 사과 세 개 — three apples | + | 명 | people | 학생 두 명 — two students | + | 살 | age | 스무 살 — 20 years old | + | 시 | o'clock | 한 시 — 1 o'clock | + | 마리 | animals | 고양이 한 마리 — one cat | + | 잔 | cups | 커피 두 잔 — two coffees | + + Time mixes both systems: **hours native, minutes Sino**. 3:20 = **세 시 이십 분**. Half past = **반**: 세 시 반. + `, + words: ` + 하나 | one (native) + 둘 | two (native) + 셋 | three (native) + 넷 | four (native) + 다섯 | five (native) + 여섯 | six (native) + 일곱 | seven (native) + 여덟 | eight (native) + 아홉 | nine (native) + 열 | ten (native) + 스물 | twenty (native) + 개 | (counter for things) + 명 | (counter for people) + 살 | years old + 시 | o'clock + 분 | minute + 반 | half + 지금 | now + `, + sentences: ` + 지금 몇 시예요? | What time is it now? + 세 시 반이에요. | It's half past three. + 몇 살이에요? | How old are you? + 스물다섯 살이에요. | I'm twenty-five. + 사과 두 개 주세요. | Two apples, please. + 가족이 몇 명이에요? | How many people are in your family? + 커피 한 잔 주세요. | One coffee, please. + `, + drills: ` + 3 people | 세 명 | 셋 명; 삼 명; 세 개 + 2 o'clock | 두 시 | 이 시; 둘 시; 두 분 + 20 years old | 스무 살 | 스물 살; 이십 살; 스무 개 + 5:30 | 다섯 시 반 | 오 시 반; 다섯 시 삼십 시; 오 시 삼십 분 + 4 apples: 사과 ___ | 네 개 | 넷 개; 사 개; 네 명 + 10:15 | 열 시 십오 분 | 십 시 십오 분; 열 시 열다섯 분; 십 시 열다섯 분 | Hours native, minutes Sino. + ` + }), + lesson({ + id: 'ko-location', + title: '어디에 있어요?', + summary: '있어요 / 없어요, location words and 에', + notes: ` + **있다** means both "to be (somewhere)" and "to have". **없다** is its opposite. + - *책이 **있어요**.* — There is a book. / I have a book. + - *시간이 **없어요**.* — I don't have time. + + ### Location: noun + 에 있어요 + **에** marks where something is: *화장실이 어디**에** 있어요?* — Where is the bathroom? + + Position words come **after** the reference noun, then 에: + *책상 **위에*** — on the desk, *집 **앞에*** — in front of the house. + + | | | + |---|---| + | 위 | on, above | + | 아래 / 밑 | under | + | 앞 | in front | + | 뒤 | behind | + | 옆 | next to | + | 안 | inside | + | 밖 | outside | + | 사이 | between | + `, + words: ` + 있다 | to be (somewhere); to have + 없다 | to not be; to not have + 어디 | where + 위 | top; on; above + 아래 | under; below + 앞 | front + 뒤 | back; behind + 옆 | side; next to + 안 | inside + 밖 | outside + 집 | house; home + 방 | room + 화장실 | bathroom; toilet + 책상 | desk + 고양이 | cat + 시간 | time + `, + sentences: ` + 화장실이 어디에 있어요? | Where is the bathroom? + 책상 위에 책이 있어요. | There's a book on the desk. + 고양이가 의자 밑에 있어요. | The cat is under the chair. + 은행 옆에 카페가 있어요. | There's a café next to the bank. + 저는 형이 있어요. | I have an older brother. + 오늘은 시간이 없어요. | I don't have time today. + 집에 아무도 없어요. | There's nobody at home. + `, + drills: ` + The phone is on the table: 휴대폰이 테이블 ___ 있어요. | 위에 | 에 위; 위를; 앞이 + I don't have money: 돈이 ___. | 없어요 | 안 있어요; 아니에요; 없다요 | The negative of 있다 is 없다. + Where is it? 어디___ 있어요? | 에 | 에서; 를; 가 + in front of the school | 학교 앞에 | 앞에 학교; 학교에 앞; 학교 앞을 + ` + }), + lesson({ + id: 'ko-eating', + title: '밥을 먹어요', + summary: 'Present tense -아요/어요 and the object particle 을/를', + notes: ` + ### The polite present: -아요 / -어요 + Take the dictionary form, remove **-다** to get the **stem**, then: + - last vowel **ㅏ or ㅗ** → add **-아요**: 받다 → 받**아요**, 좋다 → 좋**아요** + - any other vowel → add **-어요**: 먹다 → 먹**어요**, 읽다 → 읽**어요** + - **하다** → **해요**: 공부하다 → 공부**해요** + + When the stem ends in a vowel, the vowels merge: + 가다 → **가요**, 보다 → **봐요**, 마시다 → **마셔요**, 주다 → **줘요**, 배우다 → **배워요**. + + ### The object: 을 / 를 + **을** after a consonant, **를** after a vowel: *밥**을** 먹어요*, *커피**를** 마셔요*. + + ### Asking for things + **Noun + 주세요** = "please give me": *물 주세요.* + `, + words: ` + 먹다 | to eat + 마시다 | to drink + 좋아하다 | to like + 싫어하다 | to dislike + 주다 | to give + 밥 | rice; meal + 고기 | meat + 과일 | fruit + 빵 | bread + 라면 | ramyeon + 물 | water + 맥주 | beer + 맛있다 | to be delicious + 맛없다 | to taste bad + 주세요 | please give me + 많이 | a lot + 같이 | together + `, + sentences: ` + 저는 밥을 먹어요. | I eat rice. + 커피를 마셔요. | I drink coffee. + 김치를 좋아해요? | Do you like kimchi? + 이 라면 정말 맛있어요! | This ramyeon is really delicious! + 물 주세요. | Water, please. + 같이 먹어요. | Let's eat together. + 저는 고기를 안 먹어요. | I don't eat meat. + `, + drills: ` + 먹다 → | 먹어요 | 먹아요; 먹요; 먹해요 + 가다 → | 가요 | 가아요; 가어요; 갸요 + 마시다 → | 마셔요 | 마시어요; 마사요; 마시요 + 공부하다 → | 공부해요 | 공부하요; 공부하아요; 공부어요 + 보다 → | 봐요 | 보요; 버요; 보어요 + 커피___ 마셔요. | 를 | 을; 가; 는 + 빵___ 먹어요. | 을 | 를; 이; 은 + ` + }), + lesson({ + id: 'ko-going', + title: '어디에 가요?', + summary: 'Going places: 에, 에서 and (으)로', + notes: ` + ### 에 vs 에서 + - **에** — destination or location of existence: *학교**에** 가요* (I go to school), *집**에** 있어요* (I'm at home). + - **에서** — the place where an **action** happens, or "from": *학교**에서** 공부해요* (I study at school), *서울**에서** 왔어요* (I came from Seoul). + + ### (으)로 — by, towards + - Means of transport: *버스**로** 가요*, *지하철**로** 와요*. + - Direction: *오른쪽**으로** 가세요* — go to the right. + **으로** after a consonant (except ㄹ), **로** after a vowel or ㄹ: *지하철**로***, *집**으로***. + + **타다** = to ride/take (transport), with 을/를: *버스를 타요.* + `, + words: ` + 가다 | to go + 오다 | to come + 타다 | to ride; to take (transport) + 학교 | school + 회사 | company; work + 식당 | restaurant + 카페 | café + 도서관 | library + 병원 | hospital + 은행 | bank + 공원 | park + 시장 | market + 지하철 | subway + 버스 | bus + 택시 | taxi + 오른쪽 | right side + 왼쪽 | left side + `, + sentences: ` + 어디에 가요? | Where are you going? + 학교에 가요. | I'm going to school. + 도서관에서 공부해요. | I study at the library. + 지하철로 회사에 가요. | I go to work by subway. + 택시를 타요. | I take a taxi. + 어디에서 왔어요? | Where did you come from? + 오른쪽으로 가세요. | Go to the right. + `, + drills: ` + I go to the park: 공원___ 가요. | 에 | 에서; 을; 로 + I eat at a restaurant: 식당___ 먹어요. | 에서 | 에; 으로; 를 | An action happens there: 에서. + by bus: 버스___ | 로 | 으로; 에; 에서 + by subway: 지하철___ | 로 | 으로; 에서; 을 | Ends in ㄹ: 로. + to the house: 집___ 가요 (towards) | 으로 | 로; 에서; 를 + I take the bus: 버스___ 타요. | 를 | 에; 로; 가 + ` + }) + ] +); diff --git a/src/lib/language-learner/courses/ko/a2.ts b/src/lib/language-learner/courses/ko/a2.ts new file mode 100644 index 0000000..35fcd58 --- /dev/null +++ b/src/lib/language-learner/courses/ko/a2.ts @@ -0,0 +1,426 @@ +import { lesson, level } from '../../course'; + +export default level( + 'a2', + 'A2', + 'Elementary', + 'Past and future, negation, connecting sentences, abilities and honorifics.', + [ + lesson({ + id: 'ko-past', + title: '어제 뭐 했어요?', + summary: 'The past tense -았어요/었어요', + notes: ` + The past tense is built on the same 아/어 form as the present. Replace **-요** with **-ㅆ어요**: + + | dictionary | present | past | + |---|---|---| + | 가다 | 가요 | **갔어요** | + | 먹다 | 먹어요 | **먹었어요** | + | 보다 | 봐요 | **봤어요** | + | 마시다 | 마셔요 | **마셨어요** | + | 하다 | 해요 | **했어요** | + | 이다 | 이에요 | **이었어요 / 였어요** | + + Time words usually come first: *어제 영화를 봤어요.* + Useful: **어제** (yesterday), **지난주** (last week), **작년** (last year), **주말에** (at the weekend), **아까** (a moment ago). + `, + words: ` + 어제 | yesterday + 그저께 | the day before yesterday + 지난주 | last week + 작년 | last year + 주말 | weekend + 아까 | a little while ago + 영화 | movie + 보다 | to see; to watch + 만나다 | to meet + 친구 | friend + 쇼핑하다 | to go shopping + 운동하다 | to exercise + 자다 | to sleep + 일하다 | to work + `, + sentences: ` + 어제 뭐 했어요? | What did you do yesterday? + 친구를 만났어요. | I met a friend. + 주말에 영화를 봤어요. | I watched a movie at the weekend. + 아침을 먹었어요? | Did you eat breakfast? + 작년에 한국에 갔어요. | I went to Korea last year. + 어제 늦게 잤어요. | I went to bed late yesterday. + 그때 저는 학생이었어요. | Back then I was a student. + `, + drills: ` + 가다 → past | 갔어요 | 가었어요; 갔아요; 가았어요 + 먹다 → past | 먹었어요 | 먹았어요; 먹어요; 먹였어요 + 보다 → past | 봤어요 | 봐었어요; 봐어요; 봣어요 + 하다 → past | 했어요 | 하었어요; 핬어요; 햇어요 + 마시다 → past | 마셨어요 | 마시었요; 마샀어요; 마셔었어요 + 의사 + was | 의사였어요 | 의사이었요; 의사였요; 의사있었어요 + ` + }), + lesson({ + id: 'ko-future', + title: '내일 뭐 할 거예요?', + summary: 'Future -(으)ㄹ 거예요 and wanting -고 싶어요', + notes: ` + ### Future / plans: -(으)ㄹ 거예요 + - Stem ends in a vowel: **-ㄹ 거예요**: 가다 → **갈 거예요**, 하다 → **할 거예요** + - Stem ends in a consonant: **-을 거예요**: 먹다 → **먹을 거예요** + - Stem ends in ㄹ: just **거예요**: 살다 → **살 거예요** + + It also expresses a guess: *내일 비가 **올 거예요**.* — It'll probably rain tomorrow. + + ### Wanting: -고 싶어요 + Stem + **-고 싶어요** = I want to …: *한국에 가**고 싶어요**.* + For a third person, use **-고 싶어해요**: *동생이 가고 싶어해요.* + + ### Let's: -(으)ㅂ시다 / -아요 + The polite present doubles as "let's": *같이 가요!* — Let's go together! + `, + words: ` + 내일 | tomorrow + 모레 | the day after tomorrow + 다음 주 | next week + 내년 | next year + 나중에 | later + 여행 | trip; travel + 여행하다 | to travel + 계획 | plan + 배우다 | to learn + 살다 | to live + 비 | rain + 오다 | to come; (rain) to fall + 휴가 | vacation; leave + `, + sentences: ` + 내일 뭐 할 거예요? | What will you do tomorrow? + 친구하고 영화를 볼 거예요. | I'm going to watch a movie with a friend. + 내년에 서울에서 살 거예요. | I'm going to live in Seoul next year. + 한국어를 배우고 싶어요. | I want to learn Korean. + 뭐 먹고 싶어요? | What do you want to eat? + 내일 비가 올 거예요. | It will probably rain tomorrow. + 휴가 때 제주도에 가고 싶어요. | I want to go to Jeju Island during my vacation. + `, + drills: ` + 가다 → future | 갈 거예요 | 가을 거예요; 가 거예요; 갈 거에요 + 먹다 → future | 먹을 거예요 | 먹ㄹ 거예요; 멀 거예요; 먹 거예요 + 만들다 → future | 만들 거예요 | 만들을 거예요; 만드를 거예요; 만들ㄹ 거예요 | Stems in ㄹ just add 거예요. + 보다 → want to | 보고 싶어요 | 봐고 싶어요; 보 싶어요; 보고 싶아요 + I want to rest: 쉬___ | 쉬고 싶어요 | 쉬어 싶어요; 쉬고 싶어해요; 쉴 싶어요 + ` + }), + lesson({ + id: 'ko-negation', + title: '안 가요, 못 가요', + summary: 'Negation: 안, 못, -지 않다 and -지 못하다', + notes: ` + ### 안 — "don't" (choice or simple fact) + Put **안** before the verb: *안 가요* (I don't go), *안 비싸요* (it's not expensive). + For **noun + 하다** verbs, 안 goes before 하다: *공부 **안** 해요* (not ~~안 공부해요~~). + + ### 못 — "can't" (inability) + *못 가요* — I can't go. *수영 **못** 해요* — I can't swim. + + ### Long forms (more formal, written) + - stem + **-지 않아요** = 안: *가지 않아요* + - stem + **-지 못해요** = 못: *가지 못해요* + + Some verbs have their own opposite instead: **있다 ↔ 없다**, **알다 ↔ 모르다**, **이다 ↔ 아니다**, 맛있다 ↔ 맛없다. + `, + words: ` + 안 | not + 못 | cannot + 알다 | to know + 모르다 | to not know + 수영하다 | to swim + 운전하다 | to drive + 노래하다 | to sing + 매운 음식 | spicy food + 바쁘다 | to be busy + 피곤하다 | to be tired + 아프다 | to be sick; to hurt + 오늘 | today + `, + sentences: ` + 저는 고기를 안 먹어요. | I don't eat meat. + 오늘은 안 바빠요. | I'm not busy today. + 저는 수영 못 해요. | I can't swim. + 몰라요. | I don't know. + 아파서 학교에 못 갔어요. | I was sick so I couldn't go to school. + 매운 음식을 먹지 않아요. | I don't eat spicy food. + 오늘은 운동 안 했어요. | I didn't exercise today. + `, + drills: ` + I don't study: | 공부 안 해요 | 안 공부해요; 공부 못 안 해요; 공부해 안요 | 안 goes between the noun and 하다. + I can't drive: | 운전 못 해요 | 못 운전해요; 운전 안 해요; 운전 못 하요 + I don't know: | 몰라요 | 안 알아요; 몰아요; 모라요 + There is no time: 시간이 ___ | 없어요 | 안 있어요; 못 있어요; 아니에요 + 가다 → long negative | 가지 않아요 | 가지 안아요; 안 가지요; 가지 않어요 + ` + }), + lesson({ + id: 'ko-days', + title: '월요일부터 금요일까지', + summary: 'Days of the week, parts of the day, time particles', + notes: ` + ### Days of the week (요일) + | | | | + |---|---|---| + | 월요일 Monday (moon) | 화요일 Tuesday (fire) | 수요일 Wednesday (water) | + | 목요일 Thursday (wood) | 금요일 Friday (gold) | 토요일 Saturday (earth) | + | 일요일 Sunday (sun) | | | + + ### Time particles + - **에** — at/on a time: *월요일**에***, *세 시**에***. Not used with 어제, 오늘, 내일, 지금. + - **부터 … 까지** — from … until (time): *아홉 시**부터** 여섯 시**까지***. + - **에서 … 까지** — from … to (place): *서울**에서** 부산**까지***. + + Parts of the day: **아침** (morning; also breakfast), **점심** (lunchtime; lunch), **저녁** (evening; dinner), **밤** (night), **오전** (a.m.), **오후** (p.m.). + `, + words: ` + 월요일 | Monday + 화요일 | Tuesday + 수요일 | Wednesday + 목요일 | Thursday + 금요일 | Friday + 토요일 | Saturday + 일요일 | Sunday + 아침 | morning; breakfast + 점심 | lunch; midday + 저녁 | evening; dinner + 밤 | night + 오전 | a.m.; morning + 오후 | p.m.; afternoon + 매일 | every day + 부터 | from (time) + 까지 | until; to + `, + sentences: ` + 오늘은 무슨 요일이에요? | What day is it today? + 월요일부터 금요일까지 일해요. | I work from Monday to Friday. + 토요일에 뭐 해요? | What are you doing on Saturday? + 아침에 커피를 마셔요. | I drink coffee in the morning. + 저녁 먹었어요? | Did you have dinner? + 서울에서 부산까지 세 시간 걸려요. | It takes three hours from Seoul to Busan. + 오후 두 시에 만나요. | Let's meet at 2 p.m. + `, + drills: ` + on Friday: 금요일___ | 에 | 에서; 부터; 을 + tomorrow (no particle needed): ___ 가요 | 내일 | 내일에; 내일에서; 내일을 | 어제, 오늘, 내일 don't take 에. + from 9 until 5: 아홉 시___ 다섯 시까지 | 부터 | 에서; 에; 까지 + from Seoul to Busan: 서울___ 부산까지 | 에서 | 부터; 에; 로 + The day after 수요일 is: | 목요일 | 화요일; 금요일; 토요일 + ` + }), + lesson({ + id: 'ko-shopping', + title: '이거 얼마예요?', + summary: 'Shopping, counters and polite requests', + notes: ` + ### Asking for things + - **Noun + 주세요**: *이거 주세요* — This one, please. + - **Verb + -아/어 주세요** — please do (for me): *깎아 주세요* — please give me a discount, *보여 주세요* — please show me. + + ### More counters + | counter | for | + |---|---| + | 개 | general things | + | 병 | bottles | + | 잔 | cups, glasses | + | 장 | flat things: paper, tickets, photos | + | 권 | books | + | 벌 | clothes (sets) | + | 켤레 | pairs of shoes/socks | + + Paying: **카드** (card), **현금** (cash). *카드 돼요?* — Do you take cards? + `, + words: ` + 사다 | to buy + 팔다 | to sell + 비싸다 | to be expensive + 싸다 | to be cheap + 얼마예요 | how much is it + 깎다 | to cut (price); to discount + 카드 | card + 현금 | cash + 영수증 | receipt + 병 | bottle (counter) + 잔 | cup; glass (counter) + 장 | sheet (counter) + 신발 | shoes + 사이즈 | size + 다른 | other; different + 봉투 | bag; envelope + `, + sentences: ` + 이거 얼마예요? | How much is this? + 너무 비싸요. 좀 깎아 주세요. | It's too expensive. Please give me a discount. + 물 두 병 주세요. | Two bottles of water, please. + 카드 돼요? | Can I pay by card? + 다른 사이즈 있어요? | Do you have another size? + 영수증 주세요. | A receipt, please. + 봉투 필요하세요? | Do you need a bag? + 표 한 장 주세요. | One ticket, please. + `, + drills: ` + two bottles of beer: 맥주 두 ___ | 병 | 잔; 장; 개 + one ticket: 표 한 ___ | 장 | 병; 권; 명 + three books: 책 세 ___ | 권 | 장; 개; 벌 + Please show me: | 보여 주세요 | 보 주세요; 봐 주세요요; 보이 주세요 + It's cheap: | 싸요 | 사요; 싸아요; 쌌요 + ` + }), + lesson({ + id: 'ko-connecting', + title: '그리고, 그래서', + summary: 'Connecting sentences: -고, -아서/어서, 그런데', + notes: ` + ### Between sentences + | | | + |---|---| + | 그리고 | and (then) | + | 그래서 | so, therefore | + | 그런데 | but, by the way | + | 하지만 / 그렇지만 | but, however | + | 그러면 / 그럼 | then, in that case | + + ### Inside one sentence + - **-고** — and (listing, or sequence): *밥을 먹**고** 커피를 마셔요.* + - **-아서/어서** — because / and then (linked actions): *배가 고**파서** 밥을 먹었어요.* *학교에 가**서** 공부해요* (go to school and study there). + + **-아서/어서** never takes the past tense itself; the final verb carries the tense: ~~갔어서~~ → *가서*. + It also can't be used with commands or suggestions ("because it's cold, let's …" uses **-(으)니까**, see B1). + `, + words: ` + 그리고 | and + 그래서 | so + 그런데 | but; by the way + 하지만 | but; however + 그럼 | then; in that case + 배고프다 | to be hungry + 배부르다 | to be full + 늦다 | to be late + 일찍 | early + 늦게 | late + 일어나다 | to get up + 씻다 | to wash + `, + sentences: ` + 밥을 먹고 커피를 마셔요. | I eat and then drink coffee. + 배고파서 라면을 먹었어요. | I was hungry so I ate ramyeon. + 늦어서 미안해요. | Sorry I'm late. + 아침에 일어나서 씻어요. | I get up in the morning and wash. + 비가 와요. 그래서 집에 있어요. | It's raining. So I'm staying home. + 맛있어요. 그런데 좀 비싸요. | It's tasty. But it's a bit expensive. + 친구를 만나서 영화를 봤어요. | I met a friend and (we) watched a movie. + `, + drills: ` + I was busy so I couldn't come: 바___ 못 왔어요. | 빠서 | 쁘서; 빴어서; 쁘고 + 늦다 + because: | 늦어서 | 늦아서; 늦었어서; 늦고서 + It's cheap and tasty: 싸___ 맛있어요. | 고 | 서; 아서; 는데 + I'm tired. ___ I'm going to sleep. | 그래서 | 그런데; 하지만; 그리고요 + ` + }), + lesson({ + id: 'ko-can', + title: '수영할 수 있어요', + summary: 'Ability: -(으)ㄹ 수 있다 and -(으)ㄹ 줄 알다', + notes: ` + ### Can / can't: -(으)ㄹ 수 있어요 / 없어요 + Vowel stem → **-ㄹ 수 있어요**, consonant → **-을 수 있어요**, ㄹ stem → **수 있어요**: + - *갈 수 있어요* — I can go + - *먹을 수 없어요* — I can't eat + - *만들 수 있어요* — I can make + + ### Know how to: -(으)ㄹ 줄 알아요 / 몰라요 + For skills you've learned: *운전할 **줄 알아요*** — I know how to drive. *수영할 **줄 몰라요*** — I don't know how to swim. + + Short form 못 (A2 negation) also expresses inability: *못 가요* ≈ *갈 수 없어요*. + `, + words: ` + 할 수 있다 | can do + 할 수 없다 | cannot do + 할 줄 알다 | to know how to do + 피아노 | piano + 치다 | to play (piano, tennis); to hit + 기타 | guitar + 한자 | Chinese characters + 읽다 | to read + 쓰다 | to write; to use + 만들다 | to make + 도와주다 | to help + `, + sentences: ` + 한국어를 읽을 수 있어요. | I can read Korean. + 내일 올 수 있어요? | Can you come tomorrow? + 오늘은 만날 수 없어요. | I can't meet today. + 피아노를 칠 줄 알아요? | Do you know how to play the piano? + 저는 운전할 줄 몰라요. | I don't know how to drive. + 김치찌개를 만들 수 있어요. | I can make kimchi stew. + 좀 도와줄 수 있어요? | Can you help me a bit? + `, + drills: ` + 읽다 → can | 읽을 수 있어요 | 읽 수 있어요; 읽ㄹ 수 있어요; 일 수 있어요 + 가다 → can | 갈 수 있어요 | 가을 수 있어요; 가 수 있어요; 갈 수 있다요 + 만들다 → can | 만들 수 있어요 | 만들을 수 있어요; 만들ㄹ 수 있어요; 만드는 수 있어요 + I don't know how to swim: | 수영할 줄 몰라요 | 수영할 줄 안 알아요; 수영할 수 몰라요; 수영 줄 몰라요 + ` + }), + lesson({ + id: 'ko-honorifics', + title: '할머니께서 주무세요', + summary: 'Honorifics: -(으)시-, special verbs and 께서', + notes: ` + When talking **about** or **to** someone older or higher in status, Korean raises them with honorifics. + + ### -(으)세요 (honorific present) + stem + **-세요** (vowel) / **-으세요** (consonant): *가세요*, *읽으세요*. Past: **-(으)셨어요**: *가셨어요*. + This is also a polite command: *앉으세요* — please sit down. + + ### Special honorific verbs + | plain | honorific | + |---|---| + | 먹다 / 마시다 | 드시다 → 드세요 | + | 자다 | 주무시다 → 주무세요 | + | 있다 (be somewhere) | 계시다 → 계세요 | + | 말하다 | 말씀하시다 | + | 죽다 | 돌아가시다 | + + ### Honorific nouns and particles + 이/가 → **께서**, 에게 → **께**. 이름 → **성함**, 나이 → **연세**, 집 → **댁**. + Never use honorifics for yourself: *저는 가요*, not ~~저는 가세요~~. + `, + words: ` + 할머니 | grandmother + 할아버지 | grandfather + 부모님 | parents + 드시다 | to eat; to drink (honorific) + 주무시다 | to sleep (honorific) + 계시다 | to be (somewhere) (honorific) + 말씀하시다 | to speak (honorific) + 돌아가시다 | to pass away + 성함 | name (honorific) + 연세 | age (honorific) + 댁 | home (honorific) + 께서 | (honorific subject particle) + `, + sentences: ` + 할머니께서 주무세요. | Grandmother is sleeping. + 많이 드세요. | Please eat a lot. (Enjoy your meal.) + 성함이 어떻게 되세요? | May I ask your name? + 할아버지께서 댁에 계세요. | Grandfather is at home. + 연세가 어떻게 되세요? | May I ask how old you are? + 여기 앉으세요. | Please sit here. + 부모님께서 뭐 하세요? | What do your parents do? + `, + drills: ` + Grandpa is eating: 할아버지께서 ___. | 드세요 | 먹으세요; 먹어요; 마세요 + Mum is sleeping: 어머니께서 ___. | 주무세요 | 자세요; 자요; 주무어요 + The teacher is in the office: 선생님께서 사무실에 ___. | 계세요 | 있으세요; 있어요; 게세요 + Polite "sit down": | 앉으세요 | 앉세요; 앉아세요; 앉어세요 + I'm going (about yourself): 저는 ___. | 가요 | 가세요; 가셔요; 가시어요 | Never use honorifics for yourself. + ` + }) + ] +); diff --git a/src/lib/language-learner/courses/ko/b1.ts b/src/lib/language-learner/courses/ko/b1.ts new file mode 100644 index 0000000..e2a4795 --- /dev/null +++ b/src/lib/language-learner/courses/ko/b1.ts @@ -0,0 +1,404 @@ +import { lesson, level } from '../../course'; + +export default level( + 'b1', + 'B1', + 'Intermediate', + 'Obligation and permission, conditions, reasons, modifiers, experiences and the irregular verbs.', + [ + lesson({ + id: 'ko-must', + title: '해야 돼요', + summary: 'Must, may, and must not', + notes: ` + | meaning | pattern | example | + |---|---|---| + | must, have to | -아/어야 해요 / 돼요 | 가**야 돼요** — I have to go | + | may, it's okay to | -아/어도 돼요 | 먹**어도 돼요?** — May I eat? | + | must not | -(으)면 안 돼요 | 찍**으면 안 돼요** — You mustn't take (photos) | + | don't have to | 안 -아/어도 돼요 | 안 가**도 돼요** — You don't have to go | + + The 아/어 part follows the same rules as the present tense: 하다 → 해야, 가다 → 가야, 먹다 → 먹어야. + **해야 돼요** is more common in speech, **해야 해요** slightly more neutral. + `, + words: ` + -아/어야 되다 | to have to + -아/어도 되다 | to be allowed to + -(으)면 안 되다 | to not be allowed to + 사진 | photo + 찍다 | to take (a photo) + 담배 | cigarette + 피우다 | to smoke + 들어가다 | to go in + 신발을 벗다 | to take off shoes + 숙제 | homework + 내다 | to submit; to pay + 일찍 | early + `, + sentences: ` + 내일 일찍 일어나야 돼요. | I have to get up early tomorrow. + 여기서 사진을 찍어도 돼요? | May I take photos here? + 여기서 담배를 피우면 안 돼요. | You mustn't smoke here. + 신발을 벗어야 해요. | You have to take off your shoes. + 오늘 숙제를 내야 돼요. | I have to hand in my homework today. + 안 와도 돼요. | You don't have to come. + 들어가도 돼요? | May I come in? + `, + drills: ` + 가다 → must | 가야 돼요 | 가어야 돼요; 갈야 돼요; 가아야 돼요 + 먹다 → may | 먹어도 돼요 | 먹도 돼요; 먹아도 돼요; 먹으면 돼요 + 찍다 → must not | 찍으면 안 돼요 | 찍면 안 돼요; 찍어도 안 돼요; 안 찍으면 돼요 + 하다 → must | 해야 돼요 | 하야 돼요; 해도 돼요; 하어야 돼요 + You don't have to pay: | 안 내도 돼요 | 내면 안 돼요; 안 내야 돼요; 내지 마도 돼요 + ` + }), + lesson({ + id: 'ko-if-because', + title: '시간이 있으면', + summary: 'If -(으)면, because -(으)니까 and -기 때문에', + notes: ` + ### -(으)면 — if, when + *시간이 **있으면** 같이 가요.* — If you have time, let's go together. + Vowel/ㄹ stems: **-면** (가면, 살면); consonant stems: **-으면** (먹으면). + + ### Three ways to say "because" + | | use | + |---|---| + | -아/어서 | neutral cause; **not** with commands/suggestions | + | -(으)니까 | reason the speaker gives; **fine** with commands/suggestions | + | -기 때문에 | formal, emphasizes the cause | + + - *추**우니까** 창문을 닫으세요.* — It's cold, so close the window. (command → 니까) + - ~~추워서 창문을 닫으세요~~ ✗ + - Nouns: **(이)니까**, **때문에**: *비 **때문에*** — because of the rain. + `, + words: ` + -(으)면 | if; when + -(으)니까 | because; since + -기 때문에 | because + 때문에 | because of + 시간 | time + 날씨 | weather + 춥다 | to be cold + 덥다 | to be hot + 창문 | window + 닫다 | to close + 열다 | to open + 택시를 부르다 | to call a taxi + `, + sentences: ` + 시간이 있으면 같이 가요. | If you have time, let's go together. + 한국에 가면 뭐 하고 싶어요? | What do you want to do if you go to Korea? + 추우니까 창문을 닫으세요. | It's cold, so please close the window. + 늦었으니까 택시를 탑시다. | It's late, so let's take a taxi. + 비 때문에 못 갔어요. | I couldn't go because of the rain. + 일이 많기 때문에 바빠요. | I'm busy because I have a lot of work. + 모르면 물어보세요. | If you don't know, please ask. + `, + drills: ` + 먹다 → if | 먹으면 | 먹면; 먹어면; 먹으니까 + 살다 → if | 살면 | 살으면; 사면; 살아면 + It's hot, so let's go inside: | 더우니까 들어갑시다 | 더워서 들어갑시다; 덥어서 들어갑시다; 덥으니까 들어갑시다 | Suggestions need -(으)니까. + because of the traffic: 교통___ | 때문에 | 이니까요; 기 때문에; 해서 + ` + }), + lesson({ + id: 'ko-progressive', + title: '공부하고 있어요', + summary: 'Ongoing actions, intentions and purposes', + notes: ` + ### -고 있어요 — is doing (right now) + *지금 밥을 먹**고 있어요**.* — I'm eating right now. Honorific: **-고 계세요**. + Also for wearing: *안경을 쓰**고 있어요*** — he's wearing glasses. + + ### -(으)려고 해요 — intend to, plan to + *주말에 쉬**려고 해요**.* — I'm planning to rest at the weekend. + As a connector: *한국어를 배우**려고** 책을 샀어요.* — I bought a book (in order) to learn Korean. + + ### -(으)러 가다/오다 — go/come to (do) + Only with movement verbs: *밥 먹**으러** 가요* — I'm going to eat. *놀**러** 오세요!* — Come over (to hang out)! + `, + words: ` + -고 있다 | to be …ing + -(으)려고 하다 | to intend to + -(으)러 가다 | to go to (do) + 지금 | now + 요즘 | these days + 준비하다 | to prepare + 찾다 | to look for; to find + 놀다 | to play; to hang out + 이사하다 | to move (house) + 안경 | glasses + 쓰다 | to wear (glasses, hat) + `, + sentences: ` + 지금 뭐 하고 있어요? | What are you doing now? + 요즘 한국어를 배우고 있어요. | I'm learning Korean these days. + 주말에 집에서 쉬려고 해요. | I'm planning to rest at home at the weekend. + 다음 달에 이사하려고 해요. | I'm planning to move next month. + 점심 먹으러 가요. | I'm going to have lunch. + 우리 집에 놀러 오세요. | Come over to my place. + 선물을 사려고 백화점에 갔어요. | I went to the department store to buy a present. + `, + drills: ` + I'm reading now: 지금 읽___ | 고 있어요 | 어 있어요; 는 있어요; 고 계세요 | 계세요 is only for people you honour. + I intend to sleep: | 자려고 해요 | 잘려고 해요; 자으려고 해요; 자러 해요 + I'm going to eat: 먹___ 가요. | 으러 | 러; 으려고 해; 고 + I'm going to play: 놀___ 가요. | 러 | 으러; 을러; 고 | ㄹ stems take -러. + ` + }), + lesson({ + id: 'ko-modifiers', + title: '먹는 사람, 먹은 사람', + summary: 'Modifiers: turning verbs into adjectives for nouns', + notes: ` + Korean puts the whole description **before** the noun. The verb's ending shows the time: + + | | action verb | example | + |---|---|---| + | present | -는 | 먹**는** 사람 — the person who eats/is eating | + | past | -(으)ㄴ | 먹**은** 사람 — the person who ate | + | future | -(으)ㄹ | 먹**을** 사람 — the person who will eat | + + **Descriptive verbs** (adjectives) use **-(으)ㄴ** for the present: *큰 집* (big house), *좋은 사람* (good person), *예쁜 꽃*. + **있다/없다** take **-는**: *맛있**는** 음식*, *재미없**는** 영화*. + + ㄹ stems drop the ㄹ: 살다 → 사**는** 곳 (the place where I live), 멀다 → 먼 곳 (a far place). + + Relative clauses: *어제 **본** 영화* — the film I watched yesterday. *내가 **좋아하는** 노래* — the song I like. + `, + words: ` + 큰 | big (modifier) + 작은 | small (modifier) + 좋은 | good (modifier) + 예쁜 | pretty (modifier) + 맛있는 | delicious (modifier) + 재미있는 | fun (modifier) + 사는 곳 | the place where (I) live + 곳 | place + 노래 | song + 음식 | food + 옷 | clothes + 날 | day + `, + sentences: ` + 제가 좋아하는 음식은 비빔밥이에요. | The food I like is bibimbap. + 어제 본 영화가 재미있었어요. | The movie I watched yesterday was fun. + 내일 입을 옷이 없어요. | I have no clothes to wear tomorrow. + 큰 가방을 샀어요. | I bought a big bag. + 맛있는 식당을 알아요? | Do you know a good restaurant? + 사는 곳이 어디예요? | Where do you live? + 저기 서 있는 사람이 제 동생이에요. | The person standing over there is my younger sibling. + `, + drills: ` + a big house: ___ 집 | 큰 | 크는; 클; 커 + the book I'm reading: 제가 읽___ 책 | 는 | 은; 을; ㄴ + the book I read (past): 제가 읽___ 책 | 은 | 는; 을; 던은 + a tasty restaurant: 맛있___ 식당 | 는 | 은; ㄴ; 을 | 있다/없다 take -는. + the place where I live: 제가 ___ 곳 | 사는 | 살는; 살은; 산는 + the thing I will do: 제가 ___ 일 | 할 | 하는; 한; 하을 + ` + }), + lesson({ + id: 'ko-contrast', + title: '비싸지만 좋아요', + summary: 'But and background: -지만, -는데', + notes: ` + ### -지만 — but (clear contrast) + *비싸**지만** 좋아요.* — It's expensive but good. + Works with any tense: *먹었**지만** 배고파요.* + + ### -는데 / -(으)ㄴ데 — softer "but", or giving background + - Action verbs and 있다/없다: **-는데** (가는데, 먹는데, 있는데) + - Descriptive verbs: **-(으)ㄴ데** (비싼데, 좋은데) + - Nouns: **-인데** (학생인데) + - Past: **-았/었는데** (갔는데) + + Background: *배가 고픈**데** 밥 먹으러 갈까요?* — I'm hungry; shall we go eat? + Contrast: *한국어는 어려운**데** 재미있어요.* — Korean is hard, but fun. + At the end of a sentence, it softens or implies more: *좋은데요?* — It's (surprisingly) good! + `, + words: ` + -지만 | but + -는데 | but; and (background) + 어렵다 | to be difficult + 쉽다 | to be easy + 재미있다 | to be fun + 조용하다 | to be quiet + 시끄럽다 | to be noisy + 비가 오다 | to rain + 우산 | umbrella + 같이 | together + `, + sentences: ` + 비싸지만 맛있어요. | It's expensive but tasty. + 한국어는 어렵지만 재미있어요. | Korean is difficult but fun. + 비가 오는데 우산이 없어요. | It's raining and I don't have an umbrella. + 배고픈데 뭐 먹을까요? | I'm hungry; what shall we eat? + 어제 전화했는데 안 받았어요. | I called yesterday but you didn't answer. + 저는 학생인데 아르바이트도 해요. | I'm a student, but I also work part-time. + `, + drills: ` + It's small but pretty: 작___ 예뻐요. | 지만 | 는데; 은지만; 아서 + It's raining (background): 비가 오___ | 는데 | 은데; ㄴ데; 지만데 + It's expensive (background): | 비싼데 | 비싸는데; 비쌌는데요; 비싸은데 + I'm a student (background): 학생___ | 인데 | 는데; 은데; 이는데 + ` + }), + lesson({ + id: 'ko-experience', + title: '먹어 본 적 있어요?', + summary: 'Trying and experience: -아/어 보다, -(으)ㄴ 적이 있다', + notes: ` + ### -아/어 보다 — try (doing) + *이거 먹**어 보세요**.* — Try eating this. *한번 입**어 볼게요**.* — I'll try it on. + + ### -(으)ㄴ 적이 있다/없다 — have (never) done + *한국에 가 **본 적이 있어요**?* — Have you ever been to Korea? + *아니요, 가 **본 적이 없어요**.* — No, I've never been. + + Combining both (**-아/어 본 적이 있다**) is the most natural way to talk about experiences. + + **-(으)ㄹ게요** — "I'll (do it)", a promise or decision made on the spot that affects the listener: *제가 **할게요**.* — I'll do it. + `, + words: ` + -아/어 보다 | to try (doing) + -(으)ㄴ 적이 있다 | to have done before + 한번 | once; (give it) a try + 입다 | to wear; to put on (clothes) + 신다 | to wear (shoes) + 외국 | foreign country + 번 | time(s) (counter) + 처음 | first time + 경험 | experience + -(으)ㄹ게요 | I will (promise) + `, + sentences: ` + 이거 한번 먹어 보세요. | Try this. + 한국 음식을 먹어 본 적이 있어요? | Have you ever eaten Korean food? + 저는 외국에 가 본 적이 없어요. | I've never been abroad. + 이 옷을 입어 봐도 돼요? | May I try on these clothes? + 제주도에 두 번 가 봤어요. | I've been to Jeju Island twice. + 제가 전화할게요. | I'll call. + 처음 먹어 봤어요. | It's the first time I've eaten it. + `, + drills: ` + Try wearing it: 입___ 보세요. | 어 | 아; 고; 으러 + Have you ever seen it? 본 ___ 있어요? | 적이 | 것이; 일이; 수가 + I've never tried it: 해 본 적이 ___. | 없어요 | 안 있어요; 아니에요; 못 해요 + I'll wait (promise): 기다릴___ | 게요 | 거예요; 까요; 래요 + ` + }), + lesson({ + id: 'ko-irregulars', + title: 'Irregular verbs', + summary: 'ㅂ, ㄷ, 르, ㅅ and ㅎ irregulars', + notes: ` + Some verbs change their stem before a vowel. Use the **Conjugation** drill to practise all of them. + + | type | rule | example | + |---|---|---| + | ㅂ | ㅂ → 우 (돕다, 곱다: 오) | 춥다 → 추**워**요, 돕다 → 도**와**요 | + | ㄷ | ㄷ → ㄹ | 듣다 → 들**어**요, 걷다 → 걸어요 | + | 르 | 르 → ㄹ + 라/러 | 모르다 → 몰**라**요, 부르다 → 불**러**요 | + | ㅅ | ㅅ drops (no contraction) | 낫다 → 나**아**요, 짓다 → 지어요 | + | ㅎ | ㅎ drops, vowel → ㅐ | 그렇다 → 그**래**요, 빨갛다 → 빨개요 | + | 으 | ㅡ drops | 바쁘다 → 바**빠**요, 쓰다 → 써요 | + | ㄹ | ㄹ drops before ㄴ, ㅂ, ㅅ | 살다 → 사**세요**, 삽니다, 사는 | + + Not every verb that looks irregular is: **입다** (입어요), **받다** (받아요), **웃다** (웃어요), **좋다** (좋아요) are regular. + `, + words: ` + 돕다 | to help + 듣다 | to listen + 걷다 | to walk + 묻다 | to ask + 모르다 | to not know + 부르다 | to sing; to call + 빠르다 | to be fast + 다르다 | to be different + 낫다 | to get better; to be better + 짓다 | to build; to make up (a name) + 그렇다 | to be so; to be like that + 어떻다 | to be how + 맵다 | to be spicy + 무겁다 | to be heavy + 가깝다 | to be close + 귀엽다 | to be cute + `, + sentences: ` + 음악을 들어요. | I listen to music. + 날씨가 추워요. | The weather is cold. + 이 김치는 너무 매워요. | This kimchi is too spicy. + 저도 잘 몰라요. | I don't really know either. + 노래를 불러 주세요. | Please sing a song. + 감기가 다 나았어요. | My cold is completely better. + 어때요? 괜찮아요? | How is it? Is it okay? + 집이 학교에서 가까워요. | My house is close to school. + `, + drills: ` + 듣다 → | 들어요 | 듣어요; 들아요; 드어요 + 맵다 → | 매워요 | 맵어요; 매와요; 매어요 + 모르다 → | 몰라요 | 모르어요; 모라요; 몰러요 + 낫다 → | 나아요 | 낫아요; 나요; 나사요 | ㅅ drops but the vowels don't merge. + 그렇다 → | 그래요 | 그렇어요; 그러요; 그레요 + 입다 → | 입어요 | 이워요; 입아요; 이어요 | 입다 is regular. + 살다 → honorific | 사세요 | 살으세요; 살세요; 사으세요 + ` + }), + lesson({ + id: 'ko-feelings', + title: '기분이 어때요?', + summary: 'Feelings, and -아/어하다 for other people', + notes: ` + Korean distinguishes **your own** feelings from **someone else's**. + + For yourself, use the descriptive verb: *(저는) 슬퍼요* — I'm sad. + For others, add **-아/어하다** (to show signs of): *동생이 슬**퍼해요*** — my sibling is sad. + + | I feel | someone else | meaning | + |---|---|---| + | 좋다 | 좋아하다 | like | + | 싫다 | 싫어하다 | dislike | + | 무섭다 | 무서워하다 | be scared | + | 부럽다 | 부러워하다 | be envious | + | 슬프다 | 슬퍼하다 | be sad | + | 기쁘다 | 기뻐하다 | be glad | + + **기분** = mood: *기분이 좋아요* (I'm in a good mood). **화가 나다** = to get angry: *화가 났어요*. + `, + words: ` + 기분 | mood; feeling + 행복하다 | to be happy + 슬프다 | to be sad + 기쁘다 | to be glad + 화가 나다 | to get angry + 걱정하다 | to worry + 무섭다 | to be scary; to be scared + 외롭다 | to be lonely + 심심하다 | to be bored + 신나다 | to be excited + 부끄럽다 | to be embarrassed; to be shy + 속상하다 | to be upset + 설레다 | to be excited (fluttering) + 스트레스를 받다 | to be stressed + `, + sentences: ` + 오늘 기분이 어때요? | How are you feeling today? + 기분이 좋아요. | I'm in a good mood. + 왜 화가 났어요? | Why are you angry? + 걱정하지 마세요. | Don't worry. + 주말에 너무 심심했어요. | I was so bored at the weekend. + 동생이 개를 무서워해요. | My younger sibling is scared of dogs. + 요즘 스트레스를 많이 받아요. | I'm very stressed these days. + 내일 여행 가서 너무 설레요. | I'm so excited because I'm going on a trip tomorrow. + `, + drills: ` + My friend is sad: 친구가 ___. | 슬퍼해요 | 슬퍼요; 슬프해요; 슬퍼하요 | For others, use -아/어하다. + I'm scared: (저는) ___. | 무서워요 | 무서워해요; 무섭어요; 무서와요 + Don't worry: 걱정___ | 하지 마세요 | 안 하세요; 하지 않아요; 마세요 하지 + He got angry: 화가 ___. | 났어요 | 했어요; 나요; 있었어요 + ` + }) + ] +); diff --git a/src/lib/language-learner/courses/ko/b2.ts b/src/lib/language-learner/courses/ko/b2.ts new file mode 100644 index 0000000..f116bfa --- /dev/null +++ b/src/lib/language-learner/courses/ko/b2.ts @@ -0,0 +1,355 @@ +import { lesson, level } from '../../course'; + +export default level( + 'b2', + 'B2', + 'Upper intermediate', + 'Reported speech, casual speech, nuanced reasons, news vocabulary and idioms.', + [ + lesson({ + id: 'ko-reported', + title: '간다고 했어요', + summary: 'Reported speech: -다고, -냐고, -라고, -자고', + notes: ` + Reported speech changes the ending by sentence type, then adds **하다** (or 말하다, 듣다…): + + | type | action verb | descriptive verb | noun | + |---|---|---|---| + | statement | -(느)ㄴ다고 | -다고 | -(이)라고 | + | question | -냐고 | -냐고 | -(이)냐고 | + | command | -(으)라고 | — | — | + | suggestion | -자고 | — | — | + + - *민수가 내일 **간다고** 했어요.* — Minsu said he's going tomorrow. (가다 → 간다) + - *날씨가 **좋다고** 해요.* — They say the weather is nice. + - *어디 **가냐고** 물어봤어요.* — (He) asked where I was going. + - *빨리 **오라고** 했어요.* — (She) told me to come quickly. + - *같이 **먹자고** 했어요.* — (He) suggested eating together. + + Past: **-았/었다고**: *갔다고 했어요*. Contracted in speech: *간대요* (= 간다고 해요), *좋대요*, *오래요*. + "Give me" commands use **달라고**: *물을 **달라고** 했어요* — (He) asked for water. + `, + words: ` + -다고 하다 | to say that + -냐고 하다 | to ask whether + -라고 하다 | to tell (someone) to + -자고 하다 | to suggest that + -대요 | they say (contracted) + 물어보다 | to ask + 전하다 | to pass on (a message) + 소문 | rumour + 뉴스 | news + 결혼하다 | to get married + 그만두다 | to quit + `, + sentences: ` + 민수 씨가 내일 온다고 했어요. | Minsu said he's coming tomorrow. + 내일 비가 온대요. | They say it's going to rain tomorrow. + 선생님이 숙제를 하라고 하셨어요. | The teacher told us to do our homework. + 친구가 같이 영화 보자고 했어요. | My friend suggested watching a movie together. + 어디에 사냐고 물어봤어요. | They asked where I live. + 그 사람이 회사를 그만뒀다고 들었어요. | I heard that person quit the company. + 두 사람이 결혼한대요! | They say the two of them are getting married! + `, + drills: ` + He said he eats: 먹___ 했어요. | 는다고 | 다고; 냐고; 자고 + She said it's cold: 춥___ 했어요. | 다고 | 는다고; ㄴ다고; 라고 + He told me to go: 가___ 했어요. | 라고 | 자고; 다고; 냐고 + She suggested going: 가___ 했어요. | 자고 | 라고; 냐고; 는다고 + They asked if I'm a student: 학생이___ 물어봤어요. | 냐고 | 라고; 다고; 자고 + They say it's delicious: 맛있___. | 대요 | 다요; 래요; 재요 + ` + }), + lesson({ + id: 'ko-become-decide', + title: '하게 됐어요', + summary: '-게 되다, -기로 하다, -도록 and -게', + notes: ` + ### -게 되다 — end up, come to (not by one's own choice) + *회사 때문에 서울에서 살**게 됐어요**.* — I ended up living in Seoul because of work. + *김치를 좋아하**게 됐어요**.* — I've come to like kimchi. + + ### -기로 하다 — decide to + *담배를 끊**기로 했어요**.* — I decided to quit smoking. + Also for agreements: *내일 만나**기로 했어요**.* — We agreed to meet tomorrow. + + ### -도록 / -게 — so that, to the point that + *늦지 않**도록** 일찍 나가세요.* — Leave early so you're not late. + *잘 들리**게** 크게 말해 주세요.* — Please speak loudly so I can hear. + *밤새**도록*** — until the night is through. + `, + words: ` + -게 되다 | to end up; to come to + -기로 하다 | to decide to + -도록 | so that + 끊다 | to quit; to cut off + 결정하다 | to decide + 약속하다 | to promise + 취직하다 | to get a job + 유학하다 | to study abroad + 외우다 | to memorize + 들리다 | to be heard + 밤새 | all night + `, + sentences: ` + 회사 때문에 부산에 살게 됐어요. | I ended up living in Busan because of work. + 한국 드라마를 보고 한국어를 좋아하게 됐어요. | I came to love Korean after watching Korean dramas. + 올해부터 운동하기로 했어요. | I decided to start exercising this year. + 토요일에 만나기로 했어요. | We agreed to meet on Saturday. + 잊어버리지 않도록 메모하세요. | Write it down so you don't forget. + 다 들리게 크게 말해 주세요. | Please speak loudly so everyone can hear. + `, + drills: ` + I decided to go: 가___ 했어요. | 기로 | 게; 도록; 려고 + I ended up going: 가게 ___. | 됐어요 | 했어요; 있어요; 봤어요 + so that you don't catch a cold: 감기에 걸리지 않___ | 도록 | 기로; 게 되; 려고 + We agreed to meet: 만나기로 ___. | 했어요 | 됐어요; 봤어요; 있어요 + ` + }), + lesson({ + id: 'ko-banmal', + title: '반말', + summary: 'Casual speech with friends', + notes: ` + **반말** (casual speech) is used with close friends, younger people and family. Using it with strangers or elders is rude, so ask first: ***말 놓을까요?*** / ***반말해도 돼요?*** + + The basic rule: **drop the -요**. + + | polite | casual | + |---|---| + | 가요 | **가** | + | 먹었어요 | **먹었어** | + | 할 거예요 | **할 거야** | + | 학생이에요 / 의사예요 | 학생**이야** / 의사**야** | + | 네 / 아니요 | **응 / 아니** | + | 저 / 제 | **나 / 내** | + | (you) | **너 / 네** (often said 니) | + + Commands: *빨리 **와**!* Suggestions (let's): **-자**: *같이 **가자**!* + Names: *민수**야**!* (after a vowel), *지민**아**!* (after a consonant). + `, + words: ` + 반말 | casual speech + 존댓말 | polite speech + 응 | yeah + 아니 | no; nah + 나 | I (casual) + 내 | my (casual) + 너 | you (casual) + 네 | your (casual) + 뭐 해? | what are you doing? + 밥 먹었어? | have you eaten? + 진짜? | really? + 괜찮아 | it's fine + 가자 | let's go + 말을 놓다 | to switch to casual speech + `, + sentences: ` + 뭐 해? | What are you doing? + 밥 먹었어? | Have you eaten? + 응, 먹었어. | Yeah, I ate. + 내일 뭐 할 거야? | What are you doing tomorrow? + 같이 영화 보자! | Let's watch a movie together! + 나는 학생이야. | I'm a student. + 우리 말 놓을까요? | Shall we speak casually? + 지민아, 빨리 와! | Jimin, come quickly! + `, + drills: ` + 가요 → casual | 가 | 가자요; 가야; 간다요 + 먹었어요 → casual | 먹었어 | 먹었; 먹어써; 먹었야 + 의사예요 → casual | 의사야 | 의사이야; 의사예; 의사다요 + 학생이에요 → casual | 학생이야 | 학생야; 학생이에; 학생예 + Let's eat! (casual) | 먹자 | 먹어자; 먹읍시다; 먹자요 + Calling 민수: | 민수야 | 민수아; 민수씨야; 민수이야 + Calling 지민: | 지민아 | 지민야; 지민이야아; 지민씨아 + ` + }), + lesson({ + id: 'ko-cause', + title: '덕분에, 때문에', + summary: 'Good and bad reasons: 덕분에, 탓에, -는 바람에, -느라고', + notes: ` + | pattern | nuance | example | + |---|---|---| + | -(으)ㄴ/는 덕분에 | thanks to (good result) | 도와준 **덕분에** 끝냈어요 | + | -(으)ㄴ/는 탓에 | because of (blame) | 늦게 일어난 **탓에** 지각했어요 | + | -는 바람에 | because of (unexpected, bad) | 차가 막히**는 바람에** 늦었어요 | + | -느라고 | because I was busy doing | 공부하**느라고** 못 잤어요 | + + Nouns: *선생님 **덕분에***, *비 **탓에***. + **-느라고** needs the same subject in both clauses and an action verb, and the result is usually negative. + **-는 바람에** always takes the **-는** form, even for past events, and the main clause is past tense. + `, + words: ` + 덕분에 | thanks to + 탓에 | because of (blame) + -는 바람에 | because (unexpectedly) + -느라고 | because (busy) doing + 차가 막히다 | traffic is jammed + 지각하다 | to be late (for school/work) + 놓치다 | to miss (a bus, a chance) + 끝내다 | to finish + 잃어버리다 | to lose + 고장 나다 | to break down + `, + sentences: ` + 도와준 덕분에 일을 빨리 끝냈어요. | Thanks to your help I finished the work quickly. + 차가 막히는 바람에 늦었어요. | I was late because the traffic was jammed. + 게임하느라고 숙제를 못 했어요. | I couldn't do my homework because I was busy gaming. + 늦잠을 잔 탓에 버스를 놓쳤어요. | I missed the bus because I overslept. + 휴대폰이 고장 나는 바람에 연락을 못 했어요. | My phone broke, so I couldn't contact you. + 선생님 덕분에 시험에 합격했어요. | Thanks to my teacher, I passed the exam. + `, + drills: ` + Thanks to the teacher: 선생님 ___ | 덕분에 | 탓에; 바람에; 느라고 + I was busy studying and couldn't eat: 공부하___ 못 먹었어요. | 느라고 | 는 바람에; 덕분에; 기로 + The bus suddenly broke down so I was late: 버스가 고장 나___ 늦었어요. | 는 바람에 | 느라고; 덕분에; 기 위해 + ` + }), + lesson({ + id: 'ko-retrospective', + title: '가던 길, 먹었던 음식', + summary: 'Recollection: -던, -았/었던, -더라고요', + notes: ` + ### -던 — an ongoing or repeated action in the past, often unfinished + *제가 마시**던** 커피 어디 있어요?* — Where's the coffee I was drinking? + *자주 가**던** 카페* — the café I used to go to. + + ### -았/었던 — a completed past experience you're recalling + *작년에 먹**었던** 음식* — the food I ate last year (and remember). + + ### -더라고요 — reporting something you witnessed + *그 영화 정말 재미있**더라고요**.* — That movie was really fun (I saw it, I found). + Used for first-hand discoveries, not for your own actions. + `, + words: ` + -던 | that was (…ing) + -았/었던 | that (I) did before + -더라고요 | I noticed that + 자주 | often + 예전에 | in the past + 어릴 때 | in childhood + 추억 | memory + 그립다 | to miss (long for) + 동네 | neighbourhood + 옛날 | old days + `, + sentences: ` + 제가 마시던 커피 어디 있어요? | Where's the coffee I was drinking? + 어릴 때 살던 동네에 가 봤어요. | I visited the neighbourhood I used to live in as a child. + 작년에 갔던 식당에 또 갔어요. | I went again to the restaurant we went to last year. + 그 식당 음식이 정말 맛있더라고요. | The food at that restaurant was really good (I found). + 예전에 자주 듣던 노래예요. | It's a song I used to listen to often. + 한국 사람들이 정말 친절하더라고요. | Korean people were really kind (from what I saw). + `, + drills: ` + the book I was reading (unfinished): 읽___ 책 | 던 | 었던; 는; 을 + the place we visited last year: 작년에 갔___ 곳 | 던 | 는; 을; 더라 + It was really cold (I found): 정말 춥___ | 더라고요 | 던요; 었던요; 었다고요 + ` + }), + lesson({ + id: 'ko-news', + title: '뉴스와 사회', + summary: 'Society, politics and news vocabulary', + notes: ` + News Korean is heavy in **Sino-Korean** words and uses the written **-다** style (see C1). + + Headlines drop particles and end in nouns: *"대통령, 오늘 기자회견 **발표**"* — President to hold press conference today. + + Common verb pattern: **noun + 하다 / 되다** (active / passive): + *발표하다* (to announce) / *발표되다* (to be announced), *증가하다* (to increase), *감소하다* (to decrease). + `, + words: ` + 정부 | government + 대통령 | president + 국회 | National Assembly + 선거 | election + 투표하다 | to vote + 정당 | political party + 경제 | economy + 사회 | society + 기자 | reporter + 사건 | incident; case + 사고 | accident + 환경 | environment + 발표하다 | to announce + 증가하다 | to increase + 감소하다 | to decrease + 물가 | prices; cost of living + 인구 | population + 저출산 | low birth rate + `, + sentences: ` + 다음 달에 선거가 있어요. | There's an election next month. + 정부가 새로운 정책을 발표했어요. | The government announced a new policy. + 요즘 물가가 너무 올랐어요. | Prices have gone up so much lately. + 한국의 인구가 감소하고 있어요. | Korea's population is decreasing. + 저출산은 큰 사회 문제예요. | The low birth rate is a big social problem. + 어제 큰 교통사고가 났어요. | There was a big traffic accident yesterday. + 환경을 보호해야 해요. | We have to protect the environment. + `, + drills: ` + to announce | 발표하다 | 발전하다; 발견하다; 방문하다 + prices / cost of living | 물가 | 가격표; 물건; 값어치 + The population is decreasing: 인구가 ___ 있어요. | 감소하고 | 증가하고; 감소되고; 줄이고 + There was an accident: 사고가 ___. | 났어요 | 했어요; 됐어요; 있었다요 + ` + }), + lesson({ + id: 'ko-idioms', + title: '관용어와 속담', + summary: 'Idioms and proverbs', + notes: ` + ### Body idioms (관용어) + | idiom | literally | meaning | + |---|---|---| + | 발이 넓다 | feet are wide | to know a lot of people | + | 눈이 높다 | eyes are high | to have high standards | + | 손이 크다 | hands are big | to be generous (makes/gives a lot) | + | 입이 짧다 | mouth is short | to be a picky eater | + | 귀가 얇다 | ears are thin | to be easily persuaded | + | 배가 아프다 | stomach hurts | to be jealous (of someone's success) | + | 미역국을 먹다 | to eat seaweed soup | to fail an exam | + + ### Proverbs (속담) + - **누워서 떡 먹기** — eating rice cake lying down: a piece of cake. + - **식은 죽 먹기** — eating cold porridge: very easy. + - **고생 끝에 낙이 온다** — after hardship comes joy. + - **가는 말이 고와야 오는 말이 곱다** — kind words out bring kind words back. + - **원숭이도 나무에서 떨어진다** — even monkeys fall from trees. + - **낮말은 새가 듣고 밤말은 쥐가 듣는다** — birds hear daytime words, mice hear night words (walls have ears). + - **백문이 불여일견** — hearing a hundred times isn't as good as seeing once. + `, + words: ` + 발이 넓다 | to know a lot of people + 눈이 높다 | to have high standards + 손이 크다 | to be generous + 입이 짧다 | to be a picky eater + 귀가 얇다 | to be gullible + 배가 아프다 | to be jealous + 미역국을 먹다 | to fail an exam + 누워서 떡 먹기 | a piece of cake + 식은 죽 먹기 | very easy + 고생 끝에 낙이 온다 | after hardship comes happiness + 원숭이도 나무에서 떨어진다 | even experts make mistakes + 백문이 불여일견 | seeing is believing + `, + sentences: ` + 그 시험은 누워서 떡 먹기였어요. | That exam was a piece of cake. + 민수는 발이 넓어서 친구가 많아요. | Minsu knows a lot of people, so he has many friends. + 제 동생은 눈이 너무 높아요. | My younger sibling has very high standards. + 우리 어머니는 손이 커요. | My mother is very generous (always makes a lot of food). + 시험에서 미역국을 먹었어요. | I failed the exam. + 원숭이도 나무에서 떨어진다고 하잖아요. | They do say even monkeys fall from trees. + `, + drills: ` + to know a lot of people: 발이 ___ | 넓다 | 크다; 길다; 많다 + very easy: 식은 ___ 먹기 | 죽 | 밥; 떡; 국 + to fail an exam: ___을 먹다 | 미역국 | 떡국; 김치; 죽 + to have high standards: 눈이 ___ | 높다 | 크다; 넓다; 밝다 + even monkeys fall from ___ | 나무에서 | 산에서; 하늘에서; 집에서 + ` + }) + ] +); diff --git a/src/lib/language-learner/courses/ko/c1.ts b/src/lib/language-learner/courses/ko/c1.ts new file mode 100644 index 0000000..de709ae --- /dev/null +++ b/src/lib/language-learner/courses/ko/c1.ts @@ -0,0 +1,297 @@ +import { lesson, level } from '../../course'; + +export default level( + 'c1', + 'C1', + 'Advanced', + 'Written style, advanced connectors, Sino-Korean roots, sentence-final nuance and four-character idioms.', + [ + lesson({ + id: 'ko-written', + title: '한다체', + summary: 'The written plain style used in books, news and essays', + notes: ` + Newspapers, books, essays and diaries use the plain **한다체**: + + | | action verb | descriptive verb | noun | + |---|---|---|---| + | present | -(느)ㄴ다: 간다, 먹는다 | -다: 크다, 좋다 | -(이)다: 학생이다 | + | past | -았/었다: 갔다 | -았/었다: 컸다 | -였다/이었다 | + | future | -(으)ㄹ 것이다 | -(으)ㄹ 것이다 | | + | question | -는가? / -나? | -(으)ㄴ가? | | + + Written connectors: **-(으)며** (and, while), **-고자** (in order to), **및** (and), **등** (etc.), **따라서** (therefore), **그러므로** (hence), **한편** (meanwhile), **즉** (that is). + `, + words: ` + -(으)며 | and; while (written) + -고자 | in order to (formal) + 및 | and (written) + 등 | etc.; and so on + 따라서 | therefore + 그러므로 | hence + 한편 | meanwhile + 즉 | that is; namely + 예를 들어 | for example + 결론적으로 | in conclusion + 연구 | research + 결과 | result + 나타나다 | to appear; to be shown + `, + sentences: ` + 나는 매일 아침 커피를 마신다. | I drink coffee every morning. + 한국의 인구는 점점 줄어들고 있다. | Korea's population is gradually decreasing. + 이 문제를 해결하고자 한다. | (We) intend to solve this problem. + 연구 결과는 다음과 같다. | The results of the study are as follows. + 따라서 새로운 방법이 필요하다. | Therefore, a new method is needed. + 그는 가수이며 배우이다. | He is a singer and an actor. + `, + drills: ` + 가다 → 한다체 present | 간다 | 가다; 가는다; 갑니다 + 먹다 → 한다체 present | 먹는다 | 먹다; 먹ㄴ다; 먹은다 + 좋다 → 한다체 present | 좋다 | 좋는다; 좋ㄴ다; 좋은다 | Descriptive verbs keep -다. + 학생이에요 → 한다체 | 학생이다 | 학생다; 학생이는다; 학생인다 + in order to solve: 해결하___ | 고자 | 며; 는가; 고서 + ` + }), + lesson({ + id: 'ko-connectors', + title: '뿐만 아니라', + summary: 'Advanced connectors', + notes: ` + | pattern | meaning | example | + |---|---|---| + | -(으)ㄹ 뿐만 아니라 | not only … but also | **쌀 뿐만 아니라** 맛있어요 | + | -(으)ㄴ/는 반면에 | whereas, on the other hand | 형은 키가 큰 **반면에** 동생은 작아요 | + | -(으)ㄴ/는 데다가 | on top of that | 비가 오**는 데다가** 바람도 불어요 | + | -(으)ㄴ/는 대신에 | instead of; in exchange for | 영화를 보**는 대신에** 책을 읽었어요 | + | -(으)ㄹ수록 | the more … the more | 보**면 볼수록** 좋아요 | + | -(으)ㄴ/는 김에 | while (one is at it) | 나가**는 김에** 우유 좀 사 와 | + | -다 보면 | if you keep doing | 계속 연습하**다 보면** 늘 거예요 | + | -(으)ㄹ 텐데 | it would/must … (supposition) | 피곤**할 텐데** 쉬세요 | + `, + words: ` + 뿐만 아니라 | not only … but also + 반면에 | on the other hand + -는 데다가 | on top of + 대신에 | instead of + -(으)ㄹ수록 | the more … the more + -는 김에 | while at it + -다 보면 | if you keep …ing + -(으)ㄹ 텐데 | it must be … (so) + 늘다 | to improve; to increase + 바람이 불다 | the wind blows + `, + sentences: ` + 이 식당은 쌀 뿐만 아니라 맛도 있어요. | This restaurant is not only cheap but also tasty. + 서울은 복잡한 반면에 편리해요. | Seoul is hectic but on the other hand convenient. + 비가 오는 데다가 바람도 많이 불어요. | It's raining and on top of that it's very windy. + 한국어는 배우면 배울수록 재미있어요. | The more you learn Korean, the more fun it is. + 나가는 김에 쓰레기 좀 버려 줘. | Since you're going out, take the trash out. + 계속 연습하다 보면 늘 거예요. | If you keep practising, you'll improve. + 피곤할 텐데 좀 쉬세요. | You must be tired, so rest a bit. + `, + drills: ` + the more I see, the more I like: 보면 ___ 좋아요 | 볼수록 | 보는 김에; 볼 뿐; 보다 보면 + not only fast but also cheap: 빠를 ___ 싸요 | 뿐만 아니라 | 반면에; 대신에; 텐데 + Instead of rice, I ate bread: 밥 ___ 빵을 먹었어요 | 대신에 | 반면에; 김에; 뿐만 + You must be hungry (so eat): 배고플 ___ 드세요 | 텐데 | 수록; 김에; 뿐 + ` + }), + lesson({ + id: 'ko-hanja', + title: '한자어', + summary: 'Sino-Korean roots that unlock hundreds of words', + notes: ` + About 60% of Korean vocabulary comes from Chinese characters (**한자**). Learning common roots lets you guess new words. + + | root | meaning | words | + |---|---|---| + | 학 (學) | study | 학생, 학교, 대학, 과학, 학자 | + | 생 (生) | life, birth | 학생, 생일, 인생, 생활, 선생 | + | 인 (人) | person | 한국인, 외국인, 인구, 인간 | + | 대 (大) | big | 대학, 대통령, 대사관, 대회 | + | 국 (國) | country | 한국, 외국, 국가, 국제 | + | 식 (食) | eat | 식당, 음식, 식사, 한식 | + | 전 (電) | electricity | 전화, 전기, 전철, 전자 | + | 동 (東) / 서 (西) / 남 (南) / 북 (北) | east / west / south / north | 동양, 서양, 남한, 북한 | + | 시 (市) | city, market | 시장, 시민, 도시 | + | 문 (文) | writing | 문화, 문학, 문자, 논문 | + + *외국인* = 외 (outside) + 국 (country) + 인 (person) = foreigner. + `, + words: ` + 한자 | Chinese characters + 과학 | science + 인생 | life + 생활 | daily life + 외국인 | foreigner + 인간 | human + 대사관 | embassy + 국제 | international + 식사 | meal + 한식 | Korean food + 전기 | electricity + 전자 | electronic + 동양 | the East; Asia + 서양 | the West + 시민 | citizen + 도시 | city + 문화 | culture + 문학 | literature + `, + sentences: ` + 한국어 단어의 많은 부분이 한자어예요. | A large part of Korean vocabulary is Sino-Korean. + 외국인 등록증을 받았어요. | I received my foreigner registration card. + 대사관에 가야 해요. | I have to go to the embassy. + 동양 문화와 서양 문화는 많이 달라요. | Eastern and Western cultures are very different. + 식사하셨어요? | Have you eaten? (polite greeting) + 이 도시는 인구가 많아요. | This city has a large population. + `, + drills: ` + 외국인 means: | foreigner | foreign country; international; embassy + The root 식 (食) means: | eat | study; city; life + The root 학 (學) means: | study | person; big; country + Which word contains "person" (人)? | 인구 | 학교; 식당; 전화 + 국제 means: | international | domestic; national anthem; foreigner + ` + }), + lesson({ + id: 'ko-endings', + title: '잖아요, 거든요, 네요', + summary: 'Sentence endings that add nuance', + notes: ` + These endings don't change the facts, only the speaker's attitude. + + | ending | nuance | example | + |---|---|---| + | -잖아요 | "you know", reminding of shared knowledge | 내일 시험이**잖아요**. — There's a test tomorrow, you know. | + | -거든요 | giving a reason or background the listener doesn't know | 오늘 바쁘**거든요**. — (Because) I'm busy today, you see. | + | -네요 | spontaneous reaction to what you just noticed | 예쁘**네요**! — Oh, it's pretty! | + | -군요 / -구나 | realization (a bit more formal / casual) | 그랬**군요**. — I see, so that's what happened. | + | -지요 / -죠 | seeking agreement, "right?" | 맛있**죠**? — It's tasty, right? | + | -(으)ㄹ걸요 | guess, "I bet" | 벌써 갔**을걸요**. — He's probably gone already. | + | -더라고요 | reporting what you saw (B2) | 사람이 많**더라고요**. | + `, + words: ` + -잖아요 | you know (shared knowledge) + -거든요 | you see (explanation) + -네요 | oh! (discovery) + -군요 | I see (realization) + -죠 | right? (agreement) + -(으)ㄹ걸요 | I bet; probably + 벌써 | already + 아직 | still; yet + 그러니까 | so; that is why + 역시 | as expected + `, + sentences: ` + 제가 어제 말했잖아요. | I told you yesterday, you know. + 오늘은 못 가요. 약속이 있거든요. | I can't go today. I have plans, you see. + 와, 한국어를 정말 잘하시네요! | Wow, you speak Korean really well! + 아, 그래서 늦었군요. | Ah, so that's why you were late. + 이 노래 좋죠? | This song is good, right? + 지금 가면 벌써 끝났을걸요. | If we go now, it'll probably be over already. + 역시 집이 최고예요. | As expected, home is the best. + `, + drills: ` + (explaining to someone who didn't know) I'm sick, you see: 아프___ | 거든요 | 잖아요; 네요; 군요 + (reminding) It's Sunday, you know: 일요일이___ | 잖아요 | 거든요; 네요; 걸요 + (surprised discovery) Oh, it's snowing!: 눈이 오___ | 네요 | 잖아요; 거든요; 죠 + It's hot, right?: 덥___ | 죠 | 거든요; 네요; 걸요 + ` + }), + lesson({ + id: 'ko-abstract', + title: '추상 어휘', + summary: 'Abstract vocabulary for discussion', + notes: ` + Useful phrases for giving structured opinions: + - **제 생각에는 …** — In my opinion … + - **…에 대해(서)** — about …: *이 문제에 대해서 어떻게 생각하세요?* + - **…에 따라** — depending on …: *사람에 따라 달라요.* + - **…(으)ㄹ 수밖에 없다** — have no choice but to … + - **…다고 볼 수 있다** — it can be seen that … + - **반면에 / 한편으로는** — on the other hand + `, + words: ` + 개념 | concept + 관점 | point of view + 영향 | influence + 원인 | cause + 결과 | result + 문제점 | problem; drawback + 해결책 | solution + 발전 | development + 경쟁 | competition + 가치관 | values + 세대 차이 | generation gap + 인식 | perception; awareness + -에 대해서 | about + -에 따라 | depending on + -(으)ㄹ 수밖에 없다 | to have no choice but to + `, + sentences: ` + 이 문제에 대해서 어떻게 생각하세요? | What do you think about this problem? + 제 생각에는 해결책이 필요해요. | In my opinion, we need a solution. + 사람에 따라 가치관이 달라요. | Values differ from person to person. + 세대 차이 때문에 갈등이 생겨요. | Conflicts arise because of the generation gap. + 경쟁이 심해서 스트레스를 받을 수밖에 없어요. | Competition is so fierce that you can't help being stressed. + 기술 발전은 사회에 큰 영향을 미쳤어요. | Technological development has had a big influence on society. + `, + drills: ` + solution | 해결책 | 문제점; 원인; 결과 + point of view | 관점 | 개념; 인식; 영향 + depending on the person: 사람___ | 에 따라 | 에 대해서; 때문에; 밖에 + to have a big influence: 큰 영향을 ___ | 미치다 | 하다; 되다; 나다 + ` + }), + lesson({ + id: 'ko-four-char', + title: '사자성어', + summary: 'Four-character idioms', + notes: ` + **사자성어** are four-syllable idioms from Classical Chinese, common in speeches, news and among educated speakers. + + | idiom | literally | meaning | + |---|---|---| + | 일석이조 (一石二鳥) | one stone, two birds | killing two birds with one stone | + | 작심삼일 (作心三日) | resolution for three days | resolutions that don't last | + | 이심전심 (以心傳心) | from heart to heart | unspoken understanding | + | 우왕좌왕 (右往左往) | going right, going left | running around in confusion | + | 동문서답 (東問西答) | east question, west answer | an irrelevant answer | + | 대기만성 (大器晩成) | great vessels are finished late | late bloomer | + | 자업자득 (自業自得) | reap what you sow | you brought it on yourself | + | 금시초문 (今時初聞) | first hearing now | news to me | + | 십중팔구 (十中八九) | eight or nine out of ten | most likely | + | 유비무환 (有備無患) | prepared, no worries | better safe than sorry | + `, + words: ` + 사자성어 | four-character idiom + 일석이조 | two birds with one stone + 작심삼일 | a resolution that lasts three days + 이심전심 | unspoken understanding + 우왕좌왕 | running around in confusion + 동문서답 | an irrelevant answer + 대기만성 | a late bloomer + 자업자득 | you reap what you sow + 금시초문 | news to me + 십중팔구 | most likely + 유비무환 | better safe than sorry + `, + sentences: ` + 운동하면서 영어 공부도 하니까 일석이조예요. | Exercising while studying English: two birds with one stone. + 다이어트는 항상 작심삼일이에요. | My diets never last more than three days. + 그건 금시초문인데요. | That's news to me. + 십중팔구 비가 올 거예요. | It will most likely rain. + 유비무환이니까 우산을 챙기세요. | Better safe than sorry, so bring an umbrella. + 그 사람은 늘 동문서답을 해요. | That person always gives irrelevant answers. + `, + drills: ` + New Year's resolutions that fail after three days: | 작심삼일 | 일석이조; 대기만성; 유비무환 + Two birds with one stone: | 일석이조 | 이심전심; 십중팔구; 자업자득 + "That's news to me": | 금시초문 | 동문서답; 우왕좌왕; 작심삼일 + Most likely (8 or 9 out of 10): | 십중팔구 | 일석이조; 유비무환; 대기만성 + ` + }) + ] +); diff --git a/src/lib/language-learner/courses/ko/hangul.ts b/src/lib/language-learner/courses/ko/hangul.ts new file mode 100644 index 0000000..0554917 --- /dev/null +++ b/src/lib/language-learner/courses/ko/hangul.ts @@ -0,0 +1,334 @@ +import { lesson, level } from '../../course'; + +export default level( + 'hangul', + 'Hangul', + 'The alphabet', + 'Learn to read Korean in five short lessons. Everything after this is written in Hangul.', + [ + lesson({ + id: 'ko-vowels', + title: 'Basic vowels', + summary: 'ㅏ ㅓ ㅗ ㅜ ㅡ ㅣ and how syllables are built', + notes: ` + Hangul was invented in 1443 by King Sejong so that anyone could learn to read. It has 24 basic letters, grouped into **syllable blocks**. + + Every block starts with a consonant. If a syllable starts with a vowel sound, it uses the silent **ㅇ** as a placeholder: **아** = ㅇ + ㅏ = "a". + + | letter | sound | block | + |---|---|---| + | ㅏ | a (father) | 아 | + | ㅓ | eo (like "u" in "up") | 어 | + | ㅗ | o (more rounded) | 오 | + | ㅜ | u (food) | 우 | + | ㅡ | eu (unrounded "oo", lips flat) | 으 | + | ㅣ | i (see) | 이 | + + **Vertical** vowels (ㅏ ㅓ ㅣ) go to the **right** of the consonant: 아, 어, 이. + **Horizontal** vowels (ㅗ ㅜ ㅡ) go **below** it: 오, 우, 으. + `, + script: ` + ㅏ | a + ㅓ | eo | like the u in "up" + ㅗ | o + ㅜ | u + ㅡ | eu | lips spread, like a flat "oo" + ㅣ | i + 아 | a + 어 | eo + 오 | o + 우 | u + 으 | eu + 이 | i + `, + words: ` + 아이 | child + 오이 | cucumber + 이 | two; tooth + 오 | five + `, + drills: ` + Which vowel goes below the consonant? | ㅗ | ㅏ; ㅓ; ㅣ | Horizontal vowels go below. + What does ㅇ sound like at the start of a syllable? | nothing (silent) | ng; o; h + How is 어 read? | eo | a; o; u + How is 으 read? | eu | u; o; i + ` + }), + lesson({ + id: 'ko-consonants', + title: 'Basic consonants', + summary: 'ㄱ ㄴ ㄷ ㄹ ㅁ ㅂ ㅅ ㅇ ㅈ ㅎ', + notes: ` + The consonant shapes show the mouth: **ㄱ** is the tongue touching the back of the mouth, **ㄴ** the tongue on the ridge behind the teeth, **ㅁ** the closed lips. + + | letter | sound | example | + |---|---|---| + | ㄱ | g / k | 가 ga | + | ㄴ | n | 나 na | + | ㄷ | d / t | 다 da | + | ㄹ | r / l | 라 ra | + | ㅁ | m | 마 ma | + | ㅂ | b / p | 바 ba | + | ㅅ | s (sh before ㅣ) | 사 sa, 시 si | + | ㅇ | silent / ng | 아 a | + | ㅈ | j | 자 ja | + | ㅎ | h | 하 ha | + + ㄱ ㄷ ㅂ ㅈ sound softer than English k t p ch: somewhere between g/k, d/t, b/p. + ㄹ is a quick flap between r and l, like the "tt" in American "butter". + `, + script: ` + ㄱ | g; k + ㄴ | n + ㄷ | d; t + ㄹ | r; l + ㅁ | m + ㅂ | b; p + ㅅ | s + ㅈ | j + ㅎ | h + 가 | ga + 나 | na + 다 | da + 라 | ra + 마 | ma + 바 | ba + 사 | sa + 자 | ja + 하 | ha + 고 | go + 두 | du + 미 | mi + `, + words: ` + 나라 | country + 나무 | tree + 바다 | sea + 모자 | hat + 어머니 | mother + 아버지 | father + 가수 | singer + 하나 | one + 두부 | tofu + 지도 | map + 누나 | older sister (said by a male) + 라디오 | radio + `, + sentences: ` + 어머니 아버지 | mother and father + `, + drills: ` + How is 나무 read? | namu | nabu; lamu; namo + How is 바다 read? | bada | pata; bara; mada + Which letter is "m"? | ㅁ | ㅂ; ㄴ; ㅇ + Which letter is the r/l sound? | ㄹ | ㄷ; ㄴ; ㅎ + ` + }), + lesson({ + id: 'ko-strong-consonants', + title: 'Aspirated and tense consonants', + summary: 'ㅋ ㅌ ㅍ ㅊ and ㄲ ㄸ ㅃ ㅆ ㅉ', + notes: ` + Adding a stroke makes a consonant **aspirated** (with a puff of air). Doubling it makes it **tense** (no air, tight throat). + + | plain | aspirated | tense | + |---|---|---| + | ㄱ g | ㅋ k | ㄲ kk | + | ㄷ d | ㅌ t | ㄸ tt | + | ㅂ b | ㅍ p | ㅃ pp | + | ㅈ j | ㅊ ch | ㅉ jj | + | ㅅ s | | ㅆ ss | + + Hold a hand in front of your mouth: **카** should give a strong puff, **까** almost none (like the k in "skate"). + + These three-way contrasts change meaning: **불** (fire), **풀** (grass), **뿔** (horn). + `, + script: ` + ㅋ | k + ㅌ | t + ㅍ | p + ㅊ | ch + ㄲ | kk + ㄸ | tt + ㅃ | pp + ㅆ | ss + ㅉ | jj + 카 | ka + 타 | ta + 파 | pa + 차 | cha + 까 | kka + 따 | tta + 빠 | ppa + 싸 | ssa + 짜 | jja + `, + words: ` + 커피 | coffee + 차 | tea; car + 토마토 | tomato + 피자 | pizza + 아빠 | dad + 오빠 | older brother (said by a female) + 코 | nose + 치마 | skirt + 토끼 | rabbit + 기차 | train + 머리 | head; hair + 아저씨 | mister; middle-aged man + `, + sentences: ` + 커피 주세요 | Coffee, please. + `, + drills: ` + Which one is aspirated (with a puff of air)? | ㅋ | ㄲ; ㄱ; ㅇ + Which one is tense? | ㄸ | ㅌ; ㄷ; ㄹ + How is 아빠 read? | appa | aba; apa; abba + How is 치마 read? | chima | jima; kima; tima + How is 토끼 read? | tokki | dogi; toki; tokgi + ` + }), + lesson({ + id: 'ko-compound-vowels', + title: 'More vowels', + summary: 'ㅐ ㅔ, the y-vowels and the w-vowels', + notes: ` + **One extra stroke adds a "y"**: ㅏ → ㅑ (ya), ㅓ → ㅕ (yeo), ㅗ → ㅛ (yo), ㅜ → ㅠ (yu). + + **ㅐ (ae)** and **ㅔ (e)** sound almost the same today (both like "e" in "bed"). ㅒ (yae) and ㅖ (ye) likewise. + + **W-vowels** combine ㅗ or ㅜ with another vowel: + | | | | + |---|---|---| + | ㅘ = ㅗ+ㅏ | wa | 와 | + | ㅙ = ㅗ+ㅐ | wae | 왜 | + | ㅚ = ㅗ+ㅣ | oe (said "we") | 외 | + | ㅝ = ㅜ+ㅓ | wo | 워 | + | ㅞ = ㅜ+ㅔ | we | 웨 | + | ㅟ = ㅜ+ㅣ | wi | 위 | + | ㅢ = ㅡ+ㅣ | ui | 의 | + + **ㅢ** is read "ui" at the start of a word, "i" after a consonant (희 = hi), and "e" as the possessive particle 의. + `, + script: ` + ㅐ | ae + ㅔ | e + ㅑ | ya + ㅕ | yeo + ㅛ | yo + ㅠ | yu + ㅘ | wa + ㅝ | wo + ㅟ | wi + ㅢ | ui + ㅚ | oe + ㅙ | wae + 애 | ae + 에 | e + 야 | ya + 여 | yeo + 요 | yo + 유 | yu + 와 | wa + 워 | wo + 위 | wi + 의 | ui + 왜 | wae + `, + words: ` + 우유 | milk + 여우 | fox + 요리 | cooking + 개 | dog + 게 | crab + 사과 | apple + 돼지 | pig + 의자 | chair + 회사 | company + 뭐 | what + 귀 | ear + 야구 | baseball + 왜 | why + 세계 | world + `, + sentences: ` + 우유 주세요 | Milk, please. + 이게 뭐예요? | What is this? + `, + drills: ` + Adding a stroke to ㅏ gives: | ㅑ | ㅐ; ㅓ; ㅘ + ㅗ + ㅏ = | ㅘ | ㅝ; ㅚ; ㅙ + How is 의자 read? | uija | euija; wija; eja + How is 돼지 read? | dwaeji | doeji; dweji; dwaji + How is 여우 read? | yeou | yau; yeowu; yoou + ` + }), + lesson({ + id: 'ko-batchim', + title: 'Final consonants (batchim)', + summary: 'Consonants at the bottom of a block and how they link', + notes: ` + A consonant at the bottom of a block is a **batchim** (받침). Whatever the letter, a final consonant is pronounced as one of **seven sounds**: + + | sound | letters | example | + |---|---|---| + | k | ㄱ ㅋ ㄲ | 책 chaek, 부엌 bueok | + | n | ㄴ | 산 san | + | t | ㄷ ㅌ ㅅ ㅆ ㅈ ㅊ ㅎ | 옷 ot, 꽃 kkot | + | l | ㄹ | 물 mul | + | m | ㅁ | 밤 bam | + | p | ㅂ ㅍ | 밥 bap, 앞 ap | + | ng | ㅇ | 강 gang | + + ### Linking (연음) + If the next syllable starts with the silent **ㅇ**, the batchim **moves over** and is pronounced in full: + - 한국어 → [한구거] **hangugeo** + - 음악 → [으막] **eumak** + - 옷이 → [오시] **osi** + + ### Nasalization + A k/t/p sound before ㄴ or ㅁ becomes ng/n/m: **감사합니다** → [감사함니다] **gamsahamnida**. + `, + script: ` + 각 | gak + 간 | gan + 갈 | gal + 감 | gam + 갑 | gap + 강 | gang + 옷 | ot + 밖 | bak + 앞 | ap + 꽃 | kkot + `, + words: ` + 한국 | Korea + 사람 | person + 물 | water + 밥 | rice; meal + 책 | book + 빵 | bread + 김치 | kimchi + 선생님 | teacher + 학생 | student + 음식 | food + 옷 | clothes + 꽃 | flower + 서울 | Seoul + 한국어 | Korean (language) + 음악 | music + `, + sentences: ` + 감사합니다 | Thank you. + 한국 사람 | a Korean person + `, + drills: ` + The final ㅅ in 옷 sounds like: | t | s; ch; sh + The final ㅂ in 밥 sounds like: | p | b; m; silent + How is 음악 read? | eumak | eum-ak; eumag; umak | The ㅁ links to the next syllable. + How is 감사합니다 read? | gamsahamnida | gamsahapnida; kamsahabnida; gamsahabnida | ㅂ before ㄴ becomes m. + How is 한국어 read? | hangugeo | hanguk-eo; hangukeo; hangukgeo + ` + }) + ] +); diff --git a/src/lib/language-learner/courses/ko/index.ts b/src/lib/language-learner/courses/ko/index.ts new file mode 100644 index 0000000..f93dd40 --- /dev/null +++ b/src/lib/language-learner/courses/ko/index.ts @@ -0,0 +1,19 @@ +import { course } from '../../course'; +import hangul from './hangul'; +import a1 from './a1'; +import a2 from './a2'; +import b1 from './b1'; +import b2 from './b2'; +import c1 from './c1'; + +export default course( + { + code: 'ko', + name: 'Korean', + nativeName: '한국어', + glossLang: 'en', + voice: 'ko-KR', + tagline: 'Hangul first, then grammar and speech levels, taught in English.' + }, + [hangul, a1, a2, b1, b2, c1] +); diff --git a/src/lib/language-learner/courses/nl/a1.ts b/src/lib/language-learner/courses/nl/a1.ts new file mode 100644 index 0000000..8beeee6 --- /dev/null +++ b/src/lib/language-learner/courses/nl/a1.ts @@ -0,0 +1,558 @@ +import { lesson, level } from '../../course'; + +export default level( + 'a1', + 'A1', + 'Beginner', + 'Greetings, introducing yourself, numbers, family, food and getting around.', + [ + lesson({ + id: 'nl-hallo', + title: 'Hallo!', + summary: 'Greetings, goodbyes and the sounds of Dutch', + notes: ` + Dutch greetings change with the time of day. **Hoi** and **hallo** work any time; **dag** is both hello and goodbye. + + | Dutch | When | + |---|---| + | goedemorgen | until about 12:00 | + | goedemiddag | 12:00–18:00 | + | goedenavond | evening | + | welterusten | good night (going to sleep) | + + ### Sounds to know + - **g** and **ch** are the same throaty sound, like clearing your throat softly: *goed*, *acht*. + - **ui** has no English equivalent: round your lips and say "ow" (*huis*, *uit*). + - **ij** and **ei** sound the same, close to the "ay" in "day": *mij*, *klein*. + - **oe** is "oo" as in "food": *goed*. **eu** is like French *peu*: *leuk*. + - A double vowel is long: *maan* (moon) vs *man* (man). + - **w** is softer than in English, between English w and v. + + **Alsjeblieft** means both "please" and "here you are". Use **alstublieft** in formal situations. + `, + words: ` + hallo; hoi | hello; hi + dag | bye; hello | Works for both, a little formal + doei | bye | Casual + goedemorgen | good morning + goedemiddag | good afternoon + goedenavond | good evening + welterusten | good night | Only when someone goes to sleep + tot ziens | goodbye; see you + tot straks | see you later (today) + alsjeblieft | please; here you are | Informal; formal: alstublieft + dank je wel; dankjewel | thank you (very much) | Formal: dank u wel + bedankt | thanks + graag gedaan | you're welcome + ja | yes + nee | no + sorry | sorry + pardon | excuse me + `, + sentences: ` + Hoi, hoe gaat het? | Hi, how are you? + Goed, dank je. | Good, thanks. + Met mij gaat het goed. | I'm doing well. + Tot morgen! | See you tomorrow! + Hoe gaat het met u? | How are you? (formal) + Een koffie, alsjeblieft. | A coffee, please. + Dank je wel, tot ziens! | Thank you, goodbye! + `, + drills: ` + It is 09:00. You say: ___ | goedemorgen | goedenavond; welterusten; goedemiddag + It is 20:00 and you arrive at a party: ___ | goedenavond | welterusten; goedemorgen; goedemiddag + Your friend goes to bed. You say: ___ | welterusten | goedenavond; tot ziens; goedemorgen + Someone thanks you. You say: ___ | graag gedaan | alsjeblieft; sorry; pardon | Graag gedaan = "gladly done" + You hand someone their change: ___ | alsjeblieft | dank je; graag gedaan; pardon | Alsjeblieft also means "here you are". + ` + }), + lesson({ + id: 'nl-wie', + title: 'Wie ben jij?', + summary: 'Pronouns, zijn and hebben, introducing yourself', + notes: ` + ### Personal pronouns + | | singular | plural | + |---|---|---| + | 1st | ik | wij / we | + | 2nd | jij / je (informal), u (formal) | jullie | + | 3rd | hij, zij / ze, het | zij / ze | + + **Je**, **we** and **ze** are the unstressed forms and are the ones you hear most. + + ### zijn (to be) and hebben (to have) + | | zijn | hebben | + |---|---|---| + | ik | ben | heb | + | jij / je | bent | hebt | + | u | bent | hebt / heeft | + | hij / zij / het | is | heeft | + | wij / jullie / zij | zijn | hebben | + + In a question the verb moves before the subject, and **jij** loses its -t: *jij bent* → *ben jij?*, *jij hebt* → *heb jij?* + `, + words: ` + ik | I + jij; je | you (informal) + u | you (formal) + hij | he + zij; ze | she; they + wij; we | we + jullie | you (plural) + de naam | the name + heten | to be called + komen uit | to come from + wonen | to live (somewhere) + Nederland | the Netherlands + België | Belgium + Nederlands | Dutch (language) + Engels | English (language) + `, + sentences: ` + Ik heet Stan. | My name is Stan. + Wat is jouw naam? | What is your name? + Ik ben student. | I am a student. + Waar kom je vandaan? | Where are you from? + Ik kom uit Nederland. | I come from the Netherlands. + Ik woon in Amsterdam. | I live in Amsterdam. + Hij heeft een broer. | He has a brother. + Wij zijn vrienden. | We are friends. + Spreekt u Engels? | Do you speak English? + `, + drills: ` + Ik ___ student. | ben | bent; is; zijn + Jij ___ mijn vriend. | bent | ben; is; zijn + Zij ___ uit België. (komen, she) | komt | kom; komen; kome + Wij ___ een hond. | hebben | heb; hebt; heeft + Hij ___ een auto. | heeft | hebt; heb; hebben + ___ jij Nederlands? (to speak) | Spreek | Spreekt; Spreken; Spreke | Inverted jij drops the -t. + Jullie ___ in Utrecht. (to live) | wonen | woont; woon; wonent + ` + }), + lesson({ + id: 'nl-getallen', + title: 'Getallen', + summary: 'Numbers 0–100, age and phone numbers', + notes: ` + | | | | | + |---|---|---|---| + | 0 nul | 5 vijf | 10 tien | 15 vijftien | + | 1 een | 6 zes | 11 elf | 16 zestien | + | 2 twee | 7 zeven | 12 twaalf | 17 zeventien | + | 3 drie | 8 acht | 13 dertien | 18 achttien | + | 4 vier | 9 negen | 14 veertien | 19 negentien | + + Tens: twintig, dertig, veertig, vijftig, zestig, zeventig, tachtig, negentig. + + From 21 the **unit comes first**, linked by *en*, all written as one word: **eenentwintig** (one-and-twenty), **vijfenveertig** (45). + After a word ending in -e you write **ën**: *tweeëntwintig*, *drieëndertig*. + + **Hoe oud ben je?** — *Ik ben vijfentwintig (jaar).* + + When "one" is stressed it is written **één**, to tell it apart from the article *een* (a). + `, + words: ` + nul | zero + een; één | one + twee | two + drie | three + vier | four + vijf | five + zes | six + zeven | seven + acht | eight + negen | nine + tien | ten + elf | eleven + twaalf | twelve + twintig | twenty + dertig | thirty + veertig | forty + vijftig | fifty + honderd | hundred + oud | old + `, + sentences: ` + Hoe oud ben je? | How old are you? + Ik ben eenentwintig jaar. | I am twenty-one years old. + Mijn moeder is vijfenvijftig. | My mother is fifty-five. + Wat is je telefoonnummer? | What is your phone number? + Ik heb twee katten. | I have two cats. + Het kost tien euro. | It costs ten euros. + `, + drills: ` + 34 | vierendertig | drieënveertig; dertigvier; vierdertig | Unit first: four-and-thirty. + 22 | tweeëntwintig | twintigtwee; tweeentwintig; tweetwintig | Two e's meet: write ë. + 80 | tachtig | achtig; achttig; achtentig + 47 | zevenenveertig | vierenzeventig; veertigzeven; zevenveertig + 13 | dertien | drieën; drietien; dertig + ` + }), + lesson({ + id: 'nl-familie', + title: 'De familie', + summary: 'Family members, de and het, possessives', + notes: ` + ### De and het + Dutch has two words for "the": **de** and **het**. About two thirds of nouns take *de*. + + - **Plurals always take de**: *het kind* → *de kinderen*. + - People are almost always *de* (*de moeder*, *de broer*). Exception: *het kind*, *het meisje*. + - **Diminutives (-je) are always het**: *het huisje*, *het meisje*. + - Otherwise: learn every noun with its article. + + "A/an" is always **een** (pronounced like "un"). + + ### Possessives + | | | + |---|---| + | mijn | my | + | jouw / je | your | + | uw | your (formal) | + | zijn | his | + | haar | her | + | ons / onze | our | + | jullie | your (plural) | + | hun | their | + + **Ons** is used with *het* words (*ons huis*), **onze** with *de* words and plurals (*onze moeder*, *onze kinderen*). + `, + words: ` + de familie | the (extended) family + het gezin | the family (household) + de moeder | the mother + de vader | the father + de ouders | the parents + de zus | the sister + de broer | the brother + het kind | the child + de kinderen | the children + de zoon | the son + de dochter | the daughter + de oma | the grandma + de opa | the grandpa + de man | the man; the husband + de vrouw | the woman; the wife + het meisje | the girl + de jongen | the boy + getrouwd | married + `, + sentences: ` + Dit is mijn moeder. | This is my mother. + Heb je broers of zussen? | Do you have brothers or sisters? + Ik heb een zus en twee broers. | I have a sister and two brothers. + Onze ouders wonen in Groningen. | Our parents live in Groningen. + Zijn vrouw heet Anna. | His wife is called Anna. + Het kind is drie jaar oud. | The child is three years old. + Ons huis is klein. | Our house is small. + `, + drills: ` + ___ meisje | het | de | Words ending in -je are het. + ___ moeder | de | het + ___ kind | het | de + ___ kinderen | de | het | Plurals are always de. + ___ huis is groot. (our) | Ons | Onze | Huis is a het-word. + ___ kinderen zijn thuis. (our) | Onze | Ons | Plurals take onze. + Dit is ___ broer. (her) | haar | hun; zijn; hij + ` + }), + lesson({ + id: 'nl-eten', + title: 'Eten en drinken', + summary: 'Food, drink, and regular verbs in the present', + notes: ` + ### The present tense + Take the infinitive, remove **-en**: that is the **stem** and the *ik* form. + + | werken (to work) | | + |---|---| + | ik | werk | + | jij / u / hij / zij | werk**t** | + | wij / jullie / zij | werk**en** | + + Spelling rules keep the sound the same: + - A long vowel in an open syllable is written double in the stem: *maken* → *ik maak*, *eten* → *ik eet*. + - A doubled consonant becomes single: *zitten* → *ik zit*, *bakken* → *ik bak*. + - Dutch words never end in **v** or **z**: *leven* → *ik leef*, *lezen* → *ik lees*. + - A stem ending in **t** doesn't get an extra t: *ik zit*, *hij zit*. + + **Lekker** (tasty, nice) is one of the most used Dutch words: *Eet smakelijk!* is "enjoy your meal". + `, + words: ` + eten | to eat + drinken | to drink + het brood | the bread + de kaas | the cheese + de boter | the butter + het ei | the egg + de appel | the apple + het water | the water + de koffie | the coffee + de thee | the tea + de melk | the milk + het bier | the beer + de hagelslag | chocolate sprinkles + het ontbijt | the breakfast + de lunch | the lunch + het avondeten | the dinner + lekker | tasty; nice + de honger | hunger + de dorst | thirst + `, + sentences: ` + Ik heb honger. | I am hungry. + Heb jij dorst? | Are you thirsty? + Ik eet een broodje kaas. | I'm eating a cheese sandwich. + Hij drinkt koffie met melk. | He drinks coffee with milk. + Wij eten om zes uur. | We eat at six o'clock. + Dat is lekker! | That is tasty! + Eet smakelijk! | Enjoy your meal! + Mag ik een glas water? | May I have a glass of water? + `, + drills: ` + Ik ___ graag kaas. (eten) | eet | eten; eett; et + Jij ___ veel thee. (drinken) | drinkt | drink; drinken; drinkte + Hij ___ in Leiden. (wonen) | woont | wont; woon; wonen | Long vowel written double: woon-t. + Wij ___ een taart. (bakken) | bakken | bakt; bak; baken + Ik ___ een boek. (lezen) | lees | leez; lez; lezen | No word ends in z: lezen → lees. + Zij ___ op de bank. (zitten, she) | zit | zitt; zitten; zits | A stem ending in t gets no extra t. + ` + }), + lesson({ + id: 'nl-vragen', + title: 'Vragen stellen', + summary: 'Question words and word order in questions', + notes: ` + ### Question words + | | | + |---|---| + | wie | who | + | wat | what | + | waar | where | + | waarheen / waar … naartoe | where to | + | wanneer | when | + | waarom | why | + | hoe | how | + | hoeveel | how much / many | + | welk / welke | which | + + **Welk** goes with singular het-words (*welk huis*), **welke** with everything else (*welke auto*, *welke huizen*). + + ### Word order + Yes/no questions start with the verb: *Woon je hier?* + With a question word, the verb comes second: *Waar woon je?* + + Remember: **jij/je** after the verb loses its -t: *Werk jij morgen?* + `, + words: ` + wie | who + wat | what + waar | where + wanneer | when + waarom | why + hoe | how + hoeveel | how much; how many + welke | which + omdat | because + hier | here + daar | there + nu | now + vandaag | today + morgen | tomorrow; morning + `, + sentences: ` + Wie is dat? | Who is that? + Wat doe je? | What are you doing? + Waar is het station? | Where is the station? + Wanneer begint de film? | When does the film start? + Waarom lach je? | Why are you laughing? + Hoeveel kost dit? | How much does this cost? + Welke trein gaat naar Den Haag? | Which train goes to The Hague? + Werk jij morgen? | Are you working tomorrow? + `, + drills: ` + ___ kost een kaartje? | Hoeveel | Hoe; Waar; Wie + ___ woon je? – In Delft. | Waar | Wanneer; Wie; Wat + ___ huis is van jou? | Welk | Welke | Huis is a het-word, singular: welk. + ___ fiets is van jou? | Welke | Welk | Fiets is a de-word: welke. + ___ jij morgen? (werken) | Werk | Werkt; Werken; Werkte | Inversion: werk jij. + ___ ben je laat? – Omdat de trein laat was. | Waarom | Wanneer; Hoe; Wat + ` + }), + lesson({ + id: 'nl-tijd', + title: 'Dagen en tijd', + summary: 'Days of the week, months and telling time', + notes: ` + Days and months are written **without a capital** in Dutch. + + ### Telling time: the "half" trap + Dutch counts half hours **towards** the next hour: + + | time | Dutch | + |---|---| + | 2:00 | twee uur | + | 2:15 | kwart over twee | + | **2:30** | **half drie** (half *to* three) | + | 2:45 | kwart voor drie | + | 2:20 | tien voor half drie | + | 2:35 | vijf over half drie | + + **Hoe laat is het?** — What time is it? **Om hoe laat?** — At what time? + In schedules the 24-hour clock is used: *om 14.30 uur* (veertien uur dertig). + `, + words: ` + maandag | Monday + dinsdag | Tuesday + woensdag | Wednesday + donderdag | Thursday + vrijdag | Friday + zaterdag | Saturday + zondag | Sunday + het weekend | the weekend + de week | the week + de maand | the month + het jaar | the year + het uur | the hour; o'clock + de minuut | the minute + het kwartier | the quarter of an hour + half | half + gisteren | yesterday + vroeg | early + laat | late + `, + sentences: ` + Hoe laat is het? | What time is it? + Het is half acht. | It is half past seven. + De les begint om kwart over negen. | The lesson starts at a quarter past nine. + Ik werk van maandag tot vrijdag. | I work from Monday to Friday. + Op zaterdag slaap ik lang. | On Saturday I sleep in. + Tot volgende week! | See you next week! + Ik ben te laat. | I am too late. + `, + drills: ` + 3:30 = ___ | half vier | half drie; drie en half; half over drie | Half counts towards the next hour. + 9:15 = ___ | kwart over negen | kwart voor negen; kwart over tien; half negen + 6:45 = ___ | kwart voor zeven | kwart over zeven; kwart voor zes; half zeven + 11:25 = ___ | vijf voor half twaalf | vijf over half twaalf; vijf voor half elf; vijfentwintig over elf + The day after dinsdag: ___ | woensdag | donderdag; maandag; vrijdag + ` + }), + lesson({ + id: 'nl-stad', + title: 'In de stad', + summary: 'Places, directions, and er is / er zijn', + notes: ` + ### Er is / er zijn + To say something exists somewhere, use **er is** (singular) or **er zijn** (plural): + - *Er is een bakker in de straat.* — There is a baker in the street. + - *Er zijn veel fietsen.* — There are a lot of bikes. + + ### Directions + | | | + |---|---| + | rechtdoor | straight on | + | linksaf / naar links | (turn) left | + | rechtsaf / naar rechts | (turn) right | + | de eerste straat | the first street | + | op de hoek | on the corner | + | tegenover | opposite | + + **Gaan + naar** = go to: *Ik ga naar het station.* **Bij** = at (someone's place / a shop): *bij de bakker*. + `, + words: ` + de stad | the city + de straat | the street + het station | the station + de winkel | the shop + de supermarkt | the supermarket + de bakker | the baker + de bank | the bank; the couch + het plein | the square + de brug | the bridge + de gracht | the canal + de fiets | the bicycle + links | left + rechts | right + rechtdoor | straight on + ver | far + dichtbij | close by + de hoek | the corner + `, + sentences: ` + Waar is de supermarkt? | Where is the supermarket? + Ga hier linksaf. | Turn left here. + Het station is dichtbij. | The station is close by. + Er is een bakker op de hoek. | There is a baker on the corner. + Er zijn veel fietsen in Amsterdam. | There are a lot of bikes in Amsterdam. + Ik ga met de fiets naar de stad. | I'm going into town by bike. + Is het ver? | Is it far? + `, + drills: ` + Er ___ een museum in de stad. | is | zijn; heeft; hebben + Er ___ twee bakkers in deze straat. | zijn | is; hebben; heeft + Ik ga ___ het station. | naar | bij; in; op + Ik koop brood ___ de bakker. | bij | naar; aan; met + Ik ga ___ de fiets. | met | op; in; bij | Transport uses met: met de trein, met de auto. + ` + }), + lesson({ + id: 'nl-kleuren', + title: 'Kleuren en bijvoeglijke naamwoorden', + summary: 'Colours, adjectives and the -e ending', + notes: ` + ### Adjective endings + Before a noun, adjectives usually get **-e**: *de rode fiets*, *het grote huis*, *een grote auto*. + + The **only** exception: an **indefinite het-word in the singular** (after *een*, *geen*, or no article): *een groot huis*, *geen mooi weer*. + + | | de-word | het-word | + |---|---|---| + | the … | de grote auto | het grote huis | + | a … | een grote auto | een **groot** huis | + | plural | grote auto's | grote huizen | + + After the verb (*zijn*, *worden*), there is no ending: *Het huis is groot.* + + Spelling changes: *groot → grote* (long vowel in open syllable: one letter), *wit → witte* (short vowel: double consonant), *lief → lieve*, *grijs → grijze*. + `, + words: ` + rood | red + blauw | blue + geel | yellow + groen | green + zwart | black + wit | white + oranje | orange + grijs | grey + bruin | brown + roze | pink + groot | big + klein | small + mooi | beautiful + lelijk | ugly + nieuw | new + duur | expensive + goedkoop | cheap + leuk | nice; fun + `, + sentences: ` + Ik heb een rode fiets. | I have a red bicycle. + Het huis is groot. | The house is big. + Dat is een groot huis. | That is a big house. + De nieuwe auto is duur. | The new car is expensive. + Oranje is de kleur van Nederland. | Orange is the colour of the Netherlands. + Wat een mooi weer! | What lovely weather! + Ik zoek een goedkope jas. | I'm looking for a cheap coat. + `, + drills: ` + een ___ huis (groot) | groot | grote; groote; groten | Indefinite het-word: no -e. + het ___ huis (groot) | grote | groot; groote; groten + een ___ fiets (rood) | rode | rood; roode; rooie + De auto is ___. (nieuw) | nieuw | nieuwe; nieuwen; nieuws | After the verb: no ending. + de ___ muur (wit) | witte | wite; wit; wiete | Short vowel: double the consonant. + een ___ kind (lief) | lief | lieve; liefe; liev | Kind is a het-word: een lief kind. + ` + }) + ] +); diff --git a/src/lib/language-learner/courses/nl/a2.ts b/src/lib/language-learner/courses/nl/a2.ts new file mode 100644 index 0000000..ff7c9e1 --- /dev/null +++ b/src/lib/language-learner/courses/nl/a2.ts @@ -0,0 +1,503 @@ +import { lesson, level } from '../../course'; + +export default level( + 'a2', + 'A2', + 'Elementary', + 'Daily routines, the past, shopping, health and comparing things.', + [ + lesson({ + id: 'nl-dagelijks', + title: 'Mijn dag', + summary: 'Daily routine, separable and reflexive verbs', + notes: ` + ### Separable verbs + Many verbs have a prefix that **splits off and goes to the end** of the clause in the present and past: + + - *opstaan* → *Ik sta om zeven uur **op**.* + - *opbellen* → *Ik bel je morgen **op**.* + - *meenemen* → *Neem je een jas **mee**?* + + The stress is on the prefix: **op**staan, **mee**nemen. Common prefixes: *op, af, aan, uit, mee, in, terug, weg, voor*. + + ### Reflexive verbs + Some verbs need a reflexive pronoun: + + | zich wassen | | + |---|---| + | ik was **me** | wij wassen **ons** | + | jij wast **je** | jullie wassen **je** | + | hij/zij wast **zich** | zij wassen **zich** | + | u wast **zich** | | + + Common ones: *zich haasten* (hurry), *zich voelen* (feel), *zich vervelen* (be bored), *zich aankleden* (get dressed). + `, + words: ` + opstaan | to get up + wakker worden | to wake up + zich wassen | to wash (yourself) + zich aankleden | to get dressed + ontbijten | to have breakfast + vertrekken | to leave + aankomen | to arrive + opbellen | to call (on the phone) + meenemen | to take along + boodschappen doen | to do the groceries + koken | to cook + afwassen | to do the dishes + naar bed gaan | to go to bed + meestal | usually + altijd | always + nooit | never + soms | sometimes + eerst | first + daarna | after that + `, + sentences: ` + Ik sta om zeven uur op. | I get up at seven. + Daarna kleed ik me aan. | After that I get dressed. + Hij belt zijn moeder elke dag op. | He calls his mother every day. + De trein komt om negen uur aan. | The train arrives at nine. + Neem je een paraplu mee? | Are you taking an umbrella? + Ik voel me niet lekker. | I don't feel well. + We doen op zaterdag boodschappen. | We do the groceries on Saturday. + Meestal ga ik om elf uur naar bed. | I usually go to bed at eleven. + `, + drills: ` + Ik ___ om zeven uur ___. (opstaan) | sta … op | opsta … –; sta … aan; op … sta | The prefix goes to the end. + Jij wast ___. | je | me; zich; ons + Hij voelt ___ niet goed. | zich | hem; je; me + Wij haasten ___. | ons | zich; we; onze + De bus ___ om acht uur ___. (vertrekken) | vertrekt … – | trekt … ver; vertrek … –; vertrekken … – | Ver- is not separable. + Morgen bel ik je ___. | op | aan; uit; mee + ` + }), + lesson({ + id: 'nl-voltooid', + title: 'Wat heb je gedaan?', + summary: 'The perfect tense: heb gewerkt, ben gegaan', + notes: ` + The **perfect** (voltooid tegenwoordige tijd) is the usual way to talk about the past in speech: + **hebben / zijn + past participle** (at the end). + + ### Regular participles: ge- + stem + t/d + Use **-t** if the stem ends in one of the consonants of **'t kofschip** (t, k, f, s, ch, p); otherwise **-d**. + + | infinitive | stem | participle | + |---|---|---| + | werken | werk | gewerk**t** | + | wonen | woon | gewoon**d** | + | fietsen | fiets | gefiets**t** | + | leven | leef (from **v**!) | geleef**d** | + | reizen | reis (from **z**!) | gereis**d** | + + Verbs starting with **be-, ge-, her-, ver-, ont-, er-** get no extra ge-: *betaald*, *vertelt → verteld*. + + ### Strong (irregular) verbs + Often end in **-en**: *gegeten, gedronken, gezien, geschreven, gelezen*. Learn them in the verb drill. + + ### Hebben or zijn? + Most verbs use **hebben**. **Zijn** is used for a change of place or state: *ik ben gegaan, gekomen, gebleven, geworden, gestorven*. + With movement verbs (lopen, fietsen, rijden) use **zijn** when a destination is given: *Ik ben naar huis gefietst* but *Ik heb veel gefietst*. + + Separable verbs put ge- in the middle: *opgestaan*, *opgebeld*, *meegenomen*. + `, + words: ` + gisteren | yesterday + vorige week | last week + vorig jaar | last year + al | already + nog niet | not yet + net | just + ooit | ever + gewerkt | worked + gegeten | eaten + gedronken | drunk + gezien | seen + gegaan | gone + gekomen | come (participle) + geweest | been + gehad | had + gedaan | done + gekocht | bought + gelezen | read (participle) + geschreven | written + `, + sentences: ` + Wat heb je gisteren gedaan? | What did you do yesterday? + Ik heb de hele dag gewerkt. | I worked all day. + We zijn naar Rotterdam gegaan. | We went to Rotterdam. + Heb je die film al gezien? | Have you seen that film yet? + Ik ben nog nooit in België geweest. | I have never been to Belgium. + Ze heeft een nieuwe fiets gekocht. | She bought a new bike. + Hij is om zeven uur opgestaan. | He got up at seven. + Hebben jullie al gegeten? | Have you already eaten? + `, + drills: ` + Ik heb hard ___. (werken) | gewerkt | gewerkd; gewerken; werkt | k is in 't kofschip: -t. + Wij hebben in Utrecht ___. (wonen) | gewoond | gewoont; gewonen; gewoon + Hij heeft veel ___. (reizen) | gereisd | gereist; gereizd; gereisen | The stem comes from reiz-: z is not in 't kofschip. + Ik ___ naar huis gegaan. | ben | heb; heeft; is + Zij ___ een boek gelezen. (she) | heeft | is; hebt; heb + Ik heb de rekening ___. (betalen) | betaald | gebetaald; betaalt; gebetaalt | Be- verbs get no ge-. + Ze zijn gisteren ___. (aankomen) | aangekomen | geaankomen; aankomen; aangekomt + We hebben pizza ___. (eten) | gegeten | geëet; geëten; geeten + ` + }), + lesson({ + id: 'nl-verleden', + title: 'Vroeger', + summary: 'The simple past: werkte, woonde, ging', + notes: ` + The **simple past** (onvoltooid verleden tijd) is used in stories and for how things were. + + ### Regular verbs: stem + te(n) / de(n) + The same **'t kofschip** rule: stem ends in t, k, f, s, ch, p → **-te**, otherwise **-de**. + + | | werken | wonen | + |---|---|---| + | ik / jij / hij | werk**te** | woon**de** | + | wij / jullie / zij | werk**ten** | woon**den** | + + ### Strong verbs change their vowel + | infinitive | singular | plural | + |---|---|---| + | zijn | was | waren | + | hebben | had | hadden | + | gaan | ging | gingen | + | komen | kwam | kwamen | + | zien | zag | zagen | + | eten | at | aten | + | drinken | dronk | dronken | + | lezen | las | lazen | + | schrijven | schreef | schreven | + | doen | deed | deden | + + **Vroeger** (in the past, used to): *Vroeger woonde ik in Gent.* — I used to live in Ghent. + `, + words: ` + vroeger | in the past; used to + toen | then; at that time + was | was + waren | were + had | had + ging | went + kwam | came + zag | saw + at | ate + dronk | drank + woonde | lived + werkte | worked + speelde | played + deed | did + het verhaal | the story + de jeugd | the youth; childhood + `, + sentences: ` + Vroeger woonde ik in Gent. | I used to live in Ghent. + Toen ik klein was, speelde ik veel buiten. | When I was little, I played outside a lot. + Het was gisteren erg koud. | It was very cold yesterday. + We gingen elk jaar naar Frankrijk. | We went to France every year. + Hij kwam te laat op zijn werk. | He was late for work. + Ze dronken koffie en praatten lang. | They drank coffee and talked for a long time. + Ik had geen tijd. | I had no time. + `, + drills: ` + Ik ___ vroeger in een fabriek. (werken) | werkte | werkde; werkten; werkt + Wij ___ in Leiden. (wonen) | woonden | woonde; woonten; wonden + Hij ___ elke dag naar school. (gaan) | ging | gaat; gingen; gaande + Jullie ___ te laat. (komen) | kwamen | kwam; komden; komten + Ik ___ een brief. (schrijven) | schreef | schrijfde; schreven; schrijfte + Zij ___ de hele dag. (dansen, she) | danste | dansde; dansten; dans + Het ___ een mooie dag. (zijn) | was | waren; is geweest; wees + ` + }), + lesson({ + id: 'nl-winkelen', + title: 'Winkelen', + summary: 'Shopping, clothes, plurals and diminutives', + notes: ` + ### Plurals + - Most nouns: **-en**: *boek → boeken*, *fiets → fietsen*. + - Nouns ending in unstressed **-el, -em, -en, -er, -je** and many loanwords: **-s**: *tafel → tafels*, *meisje → meisjes*. + - Nouns ending in a vowel: **'s**: *auto → auto's*, *foto → foto's*. + + Spelling changes keep the vowel sound: *boom → bomen* (long), *bus → bussen* (short), *huis → huizen*, *brief → brieven*. + Some are irregular: *kind → kinderen*, *ei → eieren*, *stad → steden*. + + ### Diminutives (-je) + Dutch uses them constantly, also for things that aren't small (*een biertje*, *een momentje*). They are **always het**. + - Most words: **-je**: *huisje*, *boekje*. + - After a vowel or -l, -n, -r after long vowel: **-tje**: *autootje*, *stoeltje*, *treintje*. + - After -m: **-pje**: *boompje*. + - After a short vowel + l, m, n, r, ng: **-etje**: *balletje*, *ringetje*. + `, + words: ` + kopen | to buy + betalen | to pay + kosten | to cost + de prijs | the price + de korting | the discount + de maat | the size + passen | to try on; to fit + de kassa | the checkout + pinnen | to pay by card + contant | cash + de broek | the trousers + het shirt | the T-shirt + de jas | the coat + de schoenen | the shoes + de jurk | the dress + de tas | the bag + te groot | too big + te klein | too small + `, + sentences: ` + Hoeveel kost deze jas? | How much does this coat cost? + Mag ik dit passen? | May I try this on? + Heeft u dit in een grotere maat? | Do you have this in a bigger size? + De broek is te klein. | The trousers are too small. + Kan ik hier pinnen? | Can I pay by card here? + Ik betaal contant. | I'll pay cash. + Ik kijk alleen even rond. | I'm just looking around. + Wil je een tasje? | Would you like a bag? + `, + drills: ` + één boek, twee ___ | boeken | boeks; boekken; boekes + één tafel, twee ___ | tafels | tafelen; tafellen; tafeles + één auto, twee ___ | auto's | autos; autoën; auten + één bus, twee ___ | bussen | busen; busses; buss + één huis, twee ___ | huizen | huisen; huises; huizes + een klein huis → een ___ | huisje | huistje; huizetje; huisetje + een kleine bal → een ___ | balletje | baltje; balje; balltje + ___ boompje | het | de | Diminutives are always het. + ` + }), + lesson({ + id: 'nl-weer', + title: 'Het weer', + summary: 'Weather, seasons and talking about it', + notes: ` + The Dutch talk about the weather a lot. Weather verbs use **het**: *het regent* (it's raining), *het sneeuwt*, *het waait* (it's windy), *het vriest*. + + ### Seasons + *de lente* (spring), *de zomer*, *de herfst* (autumn), *de winter*. **In** de zomer = in summer. + + ### Adverbs of degree + *heel* / *erg* (very), *best* (quite), *een beetje* (a bit), *te* (too), *veel te* (far too). + + A famous expression: **Het regent pijpenstelen** — it's raining cats and dogs (literally "pipe stems"). + `, + words: ` + het weer | the weather + de zon | the sun + de regen | the rain + de wind | the wind + de sneeuw | the snow + de wolk | the cloud + regenen | to rain + waaien | to blow (wind) + warm | warm + koud | cold + nat | wet + droog | dry + bewolkt | cloudy + de lente | spring + de zomer | summer + de herfst | autumn + de winter | winter + de paraplu | the umbrella + `, + sentences: ` + Wat voor weer is het? | What's the weather like? + Het regent weer. | It's raining again. + Het waait hard vandaag. | It's very windy today. + Morgen wordt het zonnig. | Tomorrow it will be sunny. + In de winter is het koud. | It's cold in winter. + Het is een beetje bewolkt. | It's a bit cloudy. + Neem een paraplu mee! | Take an umbrella! + `, + drills: ` + ___ regent. | Het | Er; De; Hij + It's windy: het ___ | waait | windt; blaast; wind + In de ___ vallen de bladeren. (autumn) | herfst | lente; zomer; winter + Het is ___ koud. (far too) | veel te | te veel; heel te; zeer te + ` + }), + lesson({ + id: 'nl-gezondheid', + title: 'Bij de dokter', + summary: 'Body, health and modal verbs', + notes: ` + ### Modal verbs + | | kunnen | willen | mogen | moeten | + |---|---|---|---|---| + | ik | kan | wil | mag | moet | + | jij | kunt / kan | wilt / wil | mag | moet | + | hij | kan | wil | mag | moet | + | wij | kunnen | willen | mogen | moeten | + + The second verb goes **to the end as an infinitive**: *Ik moet naar de dokter **gaan**.* Often the *gaan* is dropped: *Ik moet naar de dokter.* + + - **kunnen** — can, be able to + - **willen** — want + - **mogen** — may, be allowed to (also: to like a person — *ik mag hem wel*) + - **moeten** — must, have to. *Niet hoeven* = don't have to: *Je hoeft niet te komen.* + + **Pijn** (pain): *Ik heb hoofdpijn, buikpijn, keelpijn.* — or *Mijn hoofd doet pijn.* + `, + words: ` + het hoofd | the head + de buik | the belly + de keel | the throat + de rug | the back + de arm | the arm + het been | the leg + de voet | the foot + de hand | the hand + het oog | the eye + de pijn | the pain + ziek | ill + gezond | healthy + de huisarts | the GP + de apotheek | the pharmacy + het medicijn | the medicine + de afspraak | the appointment + beter worden | to get better + `, + sentences: ` + Ik heb hoofdpijn. | I have a headache. + Mijn rug doet pijn. | My back hurts. + Ik moet naar de huisarts. | I have to go to the doctor. + Kan ik een afspraak maken? | Can I make an appointment? + U mag niet roken. | You're not allowed to smoke. + Je hoeft niet te werken. | You don't have to work. + Wil je iets drinken? | Do you want something to drink? + Beterschap! | Get well soon! + `, + drills: ` + Ik ___ niet komen, ik ben ziek. (kunnen) | kan | kun; kunt; kunnen + Hij ___ een glas water. (willen) | wil | wilt; willen; wilde + Wij ___ morgen vroeg opstaan. (moeten) | moeten | moet; moesten; moeten te + Je ___ niet te komen. (don't have to) | hoeft | moet; mag; kan | Niet hoeven + te = not have to. + Ik moet naar de dokter ___. | gaan | ga; gaat; te gaan | The infinitive goes to the end. + Mijn buik doet ___. | pijn | ziek; slecht; pijnlijk + ` + }), + lesson({ + id: 'nl-thuis', + title: 'Thuis', + summary: 'The house, furniture and prepositions of place', + notes: ` + ### Prepositions of place + | | | + |---|---| + | in | in | + | op | on (touching the surface) | + | aan | on (a wall), at (the table) | + | onder | under | + | boven | above | + | naast | next to | + | tussen | between | + | voor | in front of | + | achter | behind | + | bij | at, near | + + Dutch often uses a position verb instead of "is": **staan** (upright), **liggen** (flat), **zitten** (inside something), **hangen**: + *Het boek ligt op tafel. De fles staat in de koelkast. Het geld zit in mijn tas. De lamp hangt boven de tafel.* + `, + words: ` + het huis | the house + het appartement | the apartment + de kamer | the room + de woonkamer | the living room + de slaapkamer | the bedroom + de keuken | the kitchen + de badkamer | the bathroom + de wc | the toilet + de tuin | the garden + de trap | the stairs + de muur | the wall + de vloer | the floor + de bank | the couch + de kast | the cupboard + de koelkast | the fridge + het bed | the bed + het raam | the window + de deur | the door + `, + sentences: ` + Het boek ligt op tafel. | The book is on the table. + De melk staat in de koelkast. | The milk is in the fridge. + De kat zit onder het bed. | The cat is under the bed. + Er hangt een foto aan de muur. | There's a photo on the wall. + De badkamer is boven. | The bathroom is upstairs. + De sleutels zitten in mijn jas. | The keys are in my coat. + De bank staat tussen de kast en het raam. | The couch is between the cupboard and the window. + `, + drills: ` + De fles ___ op tafel. (upright) | staat | ligt; zit; hangt + De krant ___ op de bank. (flat) | ligt | staat; hangt; zit + Het schilderij hangt ___ de muur. | aan | op; in; bij + Het geld ___ in mijn portemonnee. | zit | ligt; staat; hangt | Zitten = inside something. + De kat slaapt ___ het bed. (under) | onder | boven; tussen; achter + ` + }), + lesson({ + id: 'nl-vergelijken', + title: 'Vergelijken', + summary: 'Comparatives and superlatives', + notes: ` + | | comparative (-er) | superlative (-st) | + |---|---|---| + | groot | groter | grootst | + | klein | kleiner | kleinst | + | mooi | mooier | mooist | + | duur | **duurder** | duurst | + | goed | **beter** | **best** | + | veel | **meer** | **meest** | + | weinig | **minder** | **minst** | + | graag | **liever** | **liefst** | + + Adjectives ending in **-r** add **-der**: *duurder, lekkerder, zwaarder*. + **Dan** = than: *Ik ben ouder dan jij.* **Net zo … als** = as … as: *Hij is net zo lang als ik.* + The superlative after the verb: *het grootst*: *Deze kamer is het grootst.* Before a noun: *de grootste kamer*. + + **Graag** means "gladly": *Ik drink graag thee* (I like drinking tea), *liever* (prefer), *het liefst* (like most of all). + `, + words: ` + groter | bigger + kleiner | smaller + beter | better + best | best + meer | more + minder | less + duurder | more expensive + liever | rather; prefer + dan | than + net zo … als | as … as + ouder | older + jonger | younger + sneller | faster + langzamer | slower + `, + sentences: ` + Ik ben ouder dan mijn zus. | I am older than my sister. + Amsterdam is groter dan Utrecht. | Amsterdam is bigger than Utrecht. + Dit is de beste pizza van de stad. | This is the best pizza in town. + De trein is sneller dan de bus. | The train is faster than the bus. + Ik drink liever thee dan koffie. | I prefer tea to coffee. + Hij is net zo lang als ik. | He is as tall as me. + Wat vind je het leukst? | What do you like most? + `, + drills: ` + Deze jas is ___ dan die. (duur) | duurder | duurer; meer duur; duurst | Adjectives in -r add -der. + Jouw idee is ___ dan mijn idee. (goed) | beter | goeder; best; meer goed + Ik drink ___ water dan jij. (veel) | meer | veeler; meest; veel + Dit is de ___ film van het jaar. (goed) | beste | betere; goedste; best + Hij is net zo oud ___ ik. | als | dan; zo; wie + Ik ben groter ___ jij. | dan | als; of; zo + ` + }) + ] +); diff --git a/src/lib/language-learner/courses/nl/b1.ts b/src/lib/language-learner/courses/nl/b1.ts new file mode 100644 index 0000000..fda12dd --- /dev/null +++ b/src/lib/language-learner/courses/nl/b1.ts @@ -0,0 +1,411 @@ +import { lesson, level } from '../../course'; + +export default level( + 'b1', + 'B1', + 'Intermediate', + 'Complex sentences, opinions, work, travel and the many uses of "er".', + [ + lesson({ + id: 'nl-bijzin', + title: 'Omdat, dat, als', + summary: 'Subordinate clauses: the verb goes to the end', + notes: ` + After a **subordinating conjunction** the conjugated verb moves **to the end** of the clause: + + - *Ik blijf thuis.* → *Ik blijf thuis **omdat ik ziek ben**.* + - *Ik denk **dat hij morgen komt**.* + - *Bel me **als je tijd hebt**.* + + Common subordinators: **omdat** (because), **dat** (that), **als** (if, when), **wanneer** (when), **terwijl** (while), **hoewel** (although), **voordat** (before), **nadat** (after), **zodat** (so that), **of** (whether), **totdat** (until), **sinds** (since). + + With several verbs, they cluster at the end: *…omdat ik morgen moet **werken**.* / *…omdat ik gisteren heb **gewerkt**.* + + If the subordinate clause comes **first**, the main clause starts with its verb (verb-second rule): + *Als het regent, **blijf** ik thuis.* + `, + words: ` + omdat | because + dat | that + als | if; when + terwijl | while + hoewel | although + voordat | before + nadat | after + zodat | so that + of | whether; or + totdat | until + sinds | since + zodra | as soon as + `, + sentences: ` + Ik blijf thuis omdat ik ziek ben. | I'm staying home because I'm ill. + Ik denk dat hij morgen komt. | I think he's coming tomorrow. + Bel me als je tijd hebt. | Call me if you have time. + Als het regent, blijf ik thuis. | If it rains, I'll stay home. + Hij luistert naar muziek terwijl hij werkt. | He listens to music while he works. + Weet jij of de winkel open is? | Do you know whether the shop is open? + Hoewel het koud was, gingen we zwemmen. | Although it was cold, we went swimming. + Ik was mijn handen voordat ik kook. | I wash my hands before I cook. + `, + drills: ` + Ik ga niet, omdat ik geen tijd ___. | heb | hebben; had gehad; heeft + Ik denk dat ___. | hij gelijk heeft | heeft hij gelijk; hij heeft gelijk; gelijk hij heeft + Als je honger hebt, ___ er brood. | is | er is; zijn; het is | After a subordinate clause, the main verb comes first. + Ik weet niet ___ hij komt. (whether) | of | dat; als; wanneer + Ze zegt dat ze morgen ___ ___. | moet werken | werken moet; werkt moet; moet werkt + ___ hij rijk is, is hij niet gelukkig. | Hoewel | Omdat; Zodat; Terwijl + ` + }), + lesson({ + id: 'nl-want', + title: 'Want, maar, dus', + summary: 'Coordinating conjunctions and linking ideas', + notes: ` + Some conjunctions **don't** change word order: **en** (and), **maar** (but), **of** (or), **want** (because), **dus** (so). + + ### want vs omdat + Both mean "because", but: + - *Ik blijf thuis, **want** ik **ben** ziek.* — normal word order + - *Ik blijf thuis, **omdat** ik ziek **ben**.* — verb to the end + + **Want** can never start a sentence. **Omdat** can: *Omdat ik ziek ben, blijf ik thuis.* + + ### dus + *Dus* can be followed by normal order or inversion: + *Het regent, dus ik neem de bus.* / *Het regent, dus neem ik de bus.* Both are fine. + + ### Adverbial linkers (cause inversion) + **daarom** (that's why), **toch** (still, anyway), **daarna** (after that), **bovendien** (moreover), **anders** (otherwise): + *Ik was moe. **Daarom ging** ik vroeg naar bed.* + `, + words: ` + want | because (no inversion) + maar | but + dus | so + daarom | that's why + toch | still; anyway + bovendien | moreover + anders | otherwise + eigenlijk | actually + namelijk | namely; you see + ook | also; too + niet alleen … maar ook | not only … but also + `, + sentences: ` + Ik blijf thuis, want ik ben moe. | I'm staying home because I'm tired. + Het regent, dus ik neem de bus. | It's raining, so I'm taking the bus. + Ik was moe. Daarom ging ik vroeg naar bed. | I was tired. That's why I went to bed early. + Het was duur, maar ik heb het toch gekocht. | It was expensive, but I bought it anyway. + Haast je, anders mis je de trein. | Hurry, otherwise you'll miss the train. + Ik kan niet komen, ik moet namelijk werken. | I can't come; I have to work, you see. + Eigenlijk wil ik liever thuisblijven. | Actually I'd rather stay home. + `, + drills: ` + Ik eet niets, want ___. | ik heb geen honger | ik geen honger heb; heb ik geen honger; geen honger ik heb + Ik eet niets, omdat ___. | ik geen honger heb | ik heb geen honger; heb ik geen honger; geen honger heb ik + Ik was ziek. Daarom ___ thuis. | bleef ik | ik bleef; bleef; ik ben gebleven thuis + Het was koud, ___ gingen we naar buiten. (anyway) | toch | want; omdat; daarom + ___ ik moe ben, ga ik slapen. | Omdat | Want; Dus; Maar | Want can't start a sentence. + ` + }), + lesson({ + id: 'nl-werk', + title: 'Werk en studie', + summary: 'Jobs, applying, the office', + notes: ` + ### Talking about your work + - *Wat doe je voor werk?* / *Wat voor werk doe je?* — What do you do? + - *Ik werk **bij** een bank.* — I work **at** a bank (company). + - *Ik werk **als** programmeur.* — I work **as** a programmer. + - *Ik ben programmeur.* — no article before professions! + + Dutch work culture is direct and fairly informal: colleagues use **je/jij**, and meetings (*overleggen*) are frequent. + A **ZZP'er** (zelfstandige zonder personeel) is a freelancer. + `, + words: ` + de baan | the job + het werk | the work + de collega | the colleague + de baas | the boss + de werkgever | the employer + de werknemer | the employee + het salaris | the salary + solliciteren | to apply (for a job) + de sollicitatie | the application + het sollicitatiegesprek | the job interview + het cv | the CV + de vergadering | the meeting + het overleg | the consultation; the meeting + de opleiding | the education; the training + de stage | the internship + ervaring | experience + fulltime | full-time + parttime | part-time + ontslaan | to fire + `, + sentences: ` + Wat doe je voor werk? | What do you do for a living? + Ik werk als verpleegkundige. | I work as a nurse. + Ze werkt bij een groot bedrijf. | She works at a large company. + Ik heb morgen een sollicitatiegesprek. | I have a job interview tomorrow. + Hij heeft veel ervaring met klanten. | He has a lot of experience with customers. + De vergadering duurt tot drie uur. | The meeting lasts until three o'clock. + Ik werk vier dagen per week. | I work four days a week. + Ze is vorig jaar ontslagen. | She was fired last year. + `, + drills: ` + Ik werk ___ een ziekenhuis. | bij | als; in de; voor een + Hij werkt ___ leraar. | als | bij; voor; zoals + Ik wil ___ voor die baan. (apply) | solliciteren | applyen; aanmelden; vragen + ` + }), + lesson({ + id: 'nl-betrekkelijk', + title: 'Die en dat', + summary: 'Relative clauses', + notes: ` + Relative pronouns connect a clause to a noun. The verb goes to the end: + + - **die** for **de-words** and all **plurals**: *de man **die** daar staat*, *de boeken **die** ik lees* + - **dat** for **het-words** (singular): *het huis **dat** ik koop* + + With a preposition, use **waar + preposition** for things: *de stoel **waarop** ik zit*, *de pen **waarmee** ik schrijf*. + For people use **preposition + wie**: *de vrouw **met wie** ik praat*. + + **Wat** is used after *alles, iets, niets, het enige* and superlatives: + *Alles **wat** ik weet…*, *Het mooiste **wat** ik ken…* + `, + words: ` + die | who; which; that (de-words) + dat | which; that (het-words) + wat | what; which (after alles, iets) + waarmee | with which + waarover | about which + waarop | on which + met wie | with whom + iemand | someone + iets | something + niets | nothing + alles | everything + `, + sentences: ` + De man die daar staat, is mijn buurman. | The man standing there is my neighbour. + Het boek dat ik lees, is spannend. | The book I'm reading is exciting. + De mensen die hier wonen, zijn aardig. | The people who live here are nice. + Dat is de pen waarmee ik schrijf. | That's the pen I write with. + De vrouw met wie ik praat, is lerares. | The woman I'm talking to is a teacher. + Alles wat hij zegt, is waar. | Everything he says is true. + Is er iets wat ik kan doen? | Is there anything I can do? + `, + drills: ` + de fiets ___ ik gekocht heb | die | dat; wat; wie + het huis ___ ik gekocht heb | dat | die; wat; wie + de kinderen ___ buiten spelen | die | dat; wat; wie | Plurals always take die. + alles ___ je ziet | wat | dat; die; wie + het mes ___ ik snijd (with which) | waarmee | met wat; met dat; met die + de collega ___ ik werk (with whom) | met wie | waarmee; met die; met wat + ` + }), + lesson({ + id: 'nl-toekomst', + title: 'De toekomst', + summary: 'Talking about the future: gaan, zullen, present', + notes: ` + Dutch often uses the **present tense** with a time word for the future: *Morgen **werk** ik thuis.* + + **Gaan + infinitive** — plans and predictions (like "going to"): + *Ik **ga** volgend jaar in Spanje **wonen**.* *Het **gaat** regenen.* + + **Zullen + infinitive** — promises, formal predictions, suggestions: + | zullen | | + |---|---| + | ik zal | wij zullen | + | jij zult / zal | jullie zullen | + | hij zal | zij zullen | + + - Promise: *Ik **zal** je helpen.* + - Suggestion: ***Zullen** we koffie drinken?* — Shall we have coffee? + - Probability: *Hij **zal** wel moe zijn.* — He's probably tired. + `, + words: ` + volgend jaar | next year + volgende week | next week + straks | later; soon + binnenkort | soon + later | later + zullen | shall; will + van plan zijn | to plan to + hopen | to hope + verwachten | to expect + misschien | maybe + waarschijnlijk | probably + zeker | certainly + `, + sentences: ` + Ik ga volgend jaar in Spanje wonen. | I'm going to live in Spain next year. + Het gaat regenen. | It's going to rain. + Ik zal je morgen helpen. | I'll help you tomorrow. + Zullen we een film kijken? | Shall we watch a film? + Hij zal wel moe zijn. | He's probably tired. + Ik ben van plan om te verhuizen. | I'm planning to move. + Misschien kom ik later. | Maybe I'll come later. + Ik hoop dat het morgen mooi weer is. | I hope the weather will be nice tomorrow. + `, + drills: ` + ___ we naar het strand gaan? (shall) | Zullen | Gaan; Willen; Worden + Ik ___ je morgen bellen. (promise) | zal | zul; zullen; wil + Het gaat morgen ___. (sneeuwen) | sneeuwen | sneeuwt; gesneeuwd; te sneeuwen + Ik ben van plan ___ te stoppen. | om | voor; te; dat + ` + }), + lesson({ + id: 'nl-er', + title: 'Het woordje "er"', + summary: 'The five uses of er', + notes: ` + **Er** is one of the trickiest Dutch words. It has five main jobs: + + 1. **Existence** (there is/are): ***Er** is een probleem.* + 2. **Place** (there), unstressed: *Ben je **er** al geweest?* — Have you been there yet? + 3. **Quantity** (of them), with numbers: *Ik heb **er** twee.* — I have two (of them). + 4. **With prepositions** (replaces *het* for things): *Ik denk **eraan**.* — I'm thinking about it. *Wat vind je **ervan**?* + 5. **Passive without subject**: ***Er** wordt gedanst.* — There is dancing. + + With prepositions *er* can be split: *Ik heb **er** niet **aan** gedacht.* — I didn't think of it. + + Special forms: *met → ermee*, *tot → ertoe*. + `, + words: ` + er | there; of them; it + eraan | about it; of it + ervan | of it; about it + ermee | with it + erover | about it + erop | on it + erin | in it + erbij | with it; included + denken aan | to think about + houden van | to love; to like + praten over | to talk about + `, + sentences: ` + Er is een probleem. | There's a problem. + Ben je er al geweest? | Have you been there yet? + Hoeveel katten heb je? Ik heb er twee. | How many cats do you have? I have two. + Wat vind je ervan? | What do you think of it? + Ik heb er niet aan gedacht. | I didn't think of it. + Ik ben er klaar mee. | I'm done with it. + We moeten erover praten. | We need to talk about it. + Er wordt veel gefietst in Nederland. | People cycle a lot in the Netherlands. + `, + drills: ` + Heb je appels? Ja, ik heb ___ drie. | er | ze; het; die | Quantity: er + number. + Ik denk ___. (about it) | eraan | aan het; erover het; daar | Er replaces "het" after a preposition. + Wat vind je ___? (of it) | ervan | van het; daarvan het; het van + Hij speelt ___. (with it) | ermee | met het; ermet; met er + ___ wordt gebeld. | Er | Het; Men; Daar + ` + }), + lesson({ + id: 'nl-mening', + title: 'Mijn mening', + summary: 'Giving opinions, agreeing and disagreeing', + notes: ` + The Dutch are known for being direct. Useful ways to give your opinion: + + - **Ik vind** + opinion: *Ik vind het een goed idee.* (*vinden* = to think/find, for opinions) + - **Ik denk dat** … (think, for expectations): *Ik denk dat het gaat regenen.* + - **Volgens mij** … — in my opinion: *Volgens mij heeft hij gelijk.* + - **Het eens zijn met** — to agree with: *Ik ben het met je eens.* / *Ik ben het niet met je eens.* + - **Gelijk hebben** — to be right: *Je hebt gelijk.* + + Don't confuse **vinden** (opinion) with **denken** (believe, expect): + *Ik vind die film saai* (opinion), *Ik denk dat de film om acht uur begint* (belief). + `, + words: ` + de mening | the opinion + vinden | to find; to think (opinion) + denken | to think + volgens mij | in my opinion + het eens zijn | to agree + gelijk hebben | to be right + het idee | the idea + het voordeel | the advantage + het nadeel | the disadvantage + belangrijk | important + eerlijk | honest; fair + natuurlijk | of course; natural + inderdaad | indeed + juist | exactly; correct + `, + sentences: ` + Wat vind jij ervan? | What do you think of it? + Ik vind het een goed idee. | I think it's a good idea. + Volgens mij heeft hij gelijk. | In my opinion, he's right. + Ik ben het helemaal met je eens. | I completely agree with you. + Daar ben ik het niet mee eens. | I don't agree with that. + Het voordeel is dat het goedkoop is. | The advantage is that it's cheap. + Eerlijk gezegd vind ik het saai. | To be honest, I find it boring. + `, + drills: ` + Ik ___ die film heel mooi. (opinion) | vind | denk; meen; geloof + Ik ___ dat de trein om acht uur gaat. (expectation) | denk | vind; zie; weet niet + Ik ben het ___ je eens. | met | aan; voor; bij + Je hebt ___. (you're right) | gelijk | recht; goed; juist + ___ mij is dat niet waar. | Volgens | Voor; Van; Door + ` + }), + lesson({ + id: 'nl-reizen', + title: 'Op reis', + summary: 'Travel, public transport and the OV', + notes: ` + Public transport is **het openbaar vervoer (OV)**. You check in and out with a bank card or **OV-chipkaart**: *inchecken* and *uitchecken*. + + Useful verbs: + - **overstappen** — to change (trains): *U moet in Utrecht overstappen.* + - **vertraging hebben** — to be delayed: *De trein heeft tien minuten vertraging.* + - **missen** — to miss: *Ik heb de trein gemist.* + - **halen** — to catch (make it): *Ik haal de trein niet!* + + Announcements: *"De trein naar Amsterdam Centraal vertrekt van spoor vijf."* + `, + words: ` + de reis | the trip + reizen | to travel + het openbaar vervoer | public transport + de trein | the train + het spoor | the track; the platform + het perron | the platform + de vertraging | the delay + overstappen | to change (trains) + inchecken | to check in + uitchecken | to check out + het kaartje | the ticket + de koffer | the suitcase + het vliegveld | the airport + de vlucht | the flight + het hotel | the hotel + boeken | to book + de vakantie | the holiday + het buitenland | abroad + `, + sentences: ` + De trein heeft twintig minuten vertraging. | The train is twenty minutes late. + U moet in Utrecht overstappen. | You have to change in Utrecht. + Van welk spoor vertrekt de trein? | Which platform does the train leave from? + Vergeet niet uit te checken! | Don't forget to check out! + Ik heb de laatste bus gemist. | I missed the last bus. + We gaan deze zomer naar het buitenland. | We're going abroad this summer. + Ik heb een hotel geboekt. | I booked a hotel. + `, + drills: ` + De trein heeft ___. (is delayed) | vertraging | laat; te laat; vertraagd + U moet in Amersfoort ___. (change) | overstappen | veranderen; wisselen; omstappen + Ik heb de trein ___. (missed) | gemist | gemissd; gemisst; mist + We gaan naar het ___. (abroad) | buitenland | uitland; overzee; buitenlands + ` + }) + ] +); diff --git a/src/lib/language-learner/courses/nl/b2.ts b/src/lib/language-learner/courses/nl/b2.ts new file mode 100644 index 0000000..38553a2 --- /dev/null +++ b/src/lib/language-learner/courses/nl/b2.ts @@ -0,0 +1,370 @@ +import { lesson, level } from '../../course'; + +export default level( + 'b2', + 'B2', + 'Upper intermediate', + 'Passive voice, conditionals, particles that give Dutch its flavour, and idioms.', + [ + lesson({ + id: 'nl-passief', + title: 'De lijdende vorm', + summary: 'The passive: worden and zijn', + notes: ` + The passive uses **worden** + participle (present / simple past) and **zijn** + participle (perfect): + + | tense | example | + |---|---| + | present | Het huis **wordt** gebouwd. | + | simple past | Het huis **werd** gebouwd. | + | perfect | Het huis **is** gebouwd. (not *is geworden*) | + | future | Het huis **zal** gebouwd **worden**. | + + The agent is introduced with **door**: *Het boek is **door** Mulisch geschreven.* + + Watch out: *Het huis is gebouwd* means "has been built" (perfect passive), not "is built" as a state. + + **Er**-passive without a subject: *Er wordt hier niet gerookt.* — No smoking here. + `, + words: ` + worden | to become; to be (passive) + door | by (agent) + bouwen | to build + uitvinden | to invent + ontdekken | to discover + vertalen | to translate + verkopen | to sell + sluiten | to close + stelen | to steal + repareren | to repair + gesloten | closed + gestolen | stolen + verboden | forbidden + `, + sentences: ` + Het huis wordt gebouwd. | The house is being built. + Mijn fiets is gestolen. | My bike has been stolen. + Het boek werd in 1960 geschreven. | The book was written in 1960. + Dit boek is door Mulisch geschreven. | This book was written by Mulisch. + Er wordt hier niet gerookt. | There is no smoking here. + De winkel wordt om zes uur gesloten. | The shop is closed at six. + Het probleem zal morgen opgelost worden. | The problem will be solved tomorrow. + `, + drills: ` + De brief ___ morgen verstuurd. | wordt | is; heeft; zal | Present passive: worden. + Mijn portemonnee ___ gisteren gestolen. | is | wordt; heeft; werd geworden | Perfect passive uses zijn, not "is geworden". + Het schilderij is ___ Rembrandt gemaakt. | door | van; bij; met + ___ wordt veel gelachen. | Er | Het; Men; Daar + De film ___ in 1995 gemaakt. | werd | wordt; worden; was geworden + ` + }), + lesson({ + id: 'nl-zou', + title: 'Als ik rijk was…', + summary: 'Conditionals with zou and zouden', + notes: ` + ### Hypothetical present + **Als** + simple past, main clause with **zou(den)** + infinitive: + *Als ik rijk **was**, **zou** ik een boot **kopen**.* — If I were rich, I'd buy a boat. + + ### Hypothetical past + **Als** + *had/was* + participle, main clause with **zou hebben/zijn** + participle — or simply the pluperfect in both: + *Als ik het **had geweten**, **zou** ik **zijn gekomen**.* + *Als ik het **had geweten**, **was** ik **gekomen**.* (more common in speech) + + ### Politeness + **Zou** makes requests polite: ***Zou** je het raam dicht willen doen?* + **Zouden** also reports rumours: *De minister **zou** ontslag nemen.* — The minister is said to resign. + `, + words: ` + zou | would (singular) + zouden | would (plural) + als | if + rijk | rich + graag willen | would like + in jouw plaats | in your place + stel dat | suppose that + anders | otherwise + eventueel | possibly; if need be + het geval | the case + in dat geval | in that case + `, + sentences: ` + Als ik rijk was, zou ik een boot kopen. | If I were rich, I'd buy a boat. + Ik zou graag een kamer willen reserveren. | I would like to reserve a room. + Zou je me kunnen helpen? | Could you help me? + Als ik jou was, zou ik het doen. | If I were you, I'd do it. + Als ik het had geweten, was ik gekomen. | If I'd known, I would have come. + Stel dat je de loterij wint. | Suppose you win the lottery. + Wat zou jij doen? | What would you do? + `, + drills: ` + Als ik tijd ___, zou ik je helpen. | had | heb; zou hebben; hebt + Als ik jou ___, zou ik stoppen. | was | ben; zou zijn; bent + Wij ___ graag willen komen. | zouden | zou; zullen; zul + Als ik het had geweten, ___ ik gebleven. | was | had; ben; werd | Blijven takes zijn. + ___ u iets harder kunnen praten? | Zou | Zal; Wil; Kan + ` + }), + lesson({ + id: 'nl-voornaamwoordelijk', + title: 'Daarover, waarmee', + summary: 'Pronominal adverbs and verbs with fixed prepositions', + notes: ` + Many Dutch verbs have a **fixed preposition**: + + | | | + |---|---| + | wachten **op** | to wait for | + | denken **aan** | to think of/about | + | houden **van** | to love | + | zorgen **voor** | to take care of | + | praten **over** | to talk about | + | geïnteresseerd zijn **in** | to be interested in | + | bang zijn **voor** | to be afraid of | + | trots zijn **op** | to be proud of | + | boos zijn **op** | to be angry with | + | zin hebben **in** | to feel like | + + For **things**, the preposition + pronoun becomes one word: **er-, daar-, waar-** + preposition: + - *Ik wacht **erop**.* — I'm waiting for it. + - ***Daar** heb ik geen zin **in**.* — I don't feel like that. + - ***Waar** denk je **aan**?* — What are you thinking about? + + For people, keep preposition + pronoun: *Ik wacht **op hem**.* + `, + words: ` + wachten op | to wait for + denken aan | to think of + houden van | to love + zorgen voor | to take care of + geïnteresseerd zijn in | to be interested in + bang zijn voor | to be afraid of + trots zijn op | to be proud of + boos zijn op | to be angry with + zin hebben in | to feel like + genieten van | to enjoy + afhangen van | to depend on + twijfelen aan | to doubt + `, + sentences: ` + Waar wacht je op? | What are you waiting for? + Ik heb geen zin in werken. | I don't feel like working. + Daar heb ik nooit aan gedacht. | I never thought of that. + Ze is erg trots op haar dochter. | She is very proud of her daughter. + Het hangt ervan af. | It depends. + Waar ben je bang voor? | What are you afraid of? + Ik geniet ervan. | I'm enjoying it. + Hij zorgt voor zijn moeder. | He takes care of his mother. + `, + drills: ` + Ik wacht ___ de bus. | op | voor; naar; aan + Ze houdt ___ chocola. | van | op; met; aan + Hij is geïnteresseerd ___ kunst. | in | voor; aan; op + Ik denk ___. (of it) | eraan | aan het; eraf; erop + ___ ben je boos op? (what) | Waar | Wat; Wie; Waarop + Heb je ___ in pizza? | zin | lust; honger; wil + ` + }), + lesson({ + id: 'nl-partikels', + title: 'Toch, even, maar, wel', + summary: 'Modal particles: the words that make Dutch sound natural', + notes: ` + Short words like **even, maar, toch, wel, eens, hoor, nou, gewoon** don't change the facts but change the **tone**. Natives use them constantly. + + | particle | effect | example | + |---|---|---| + | **even** | "just quickly", softens a request | *Kun je **even** helpen?* | + | **maar** | encouraging, "go ahead" | *Kom **maar** binnen.* | + | **eens** | friendly suggestion | *Kijk **eens**!* | + | **toch** | "surely, right?", seeking agreement | *Je komt **toch**?* | + | **wel** | contrast, "actually", reassurance | *Het komt **wel** goed.* | + | **hoor** | reassurance, softens a statement | *Dat is goed, **hoor**.* | + | **nou** | impatience or emphasis | ***Nou**, en?* | + | **gewoon** | "simply, just" | *Doe **gewoon** normaal.* | + + They're often stacked: *Kom **toch maar eens** langs!* — Do come by sometime! + `, + words: ` + even | just (for a moment) + maar | just; go ahead + toch | surely; still; after all + wel | actually; indeed (contrast) + eens | once; sometime + hoor | you know (reassurance) + nou | well; now + gewoon | just; simply; normal + zeker | certainly; surely + heus | really; truly + nogal | rather; quite + best | quite; fine + `, + sentences: ` + Kun je even helpen? | Could you just help for a moment? + Kom maar binnen. | Come on in. + Je komt toch ook? | You're coming too, right? + Het komt wel goed. | It'll be fine. + Dat is niet erg, hoor. | That's okay, don't worry. + Doe gewoon normaal. | Just act normal. + Kom eens langs! | Come by some time! + Het is nogal duur. | It's rather expensive. + `, + drills: ` + Kom ___ binnen, de deur is open. (encouraging) | maar | even; toch; nou + Kun je ___ het zout geven? (quick request) | even | maar; toch; nou + Ik ben niet rijk, maar ik ben ___ gelukkig. (contrast) | wel | toch; even; hoor + Je weet het ___ ook? (surely) | toch | wel; maar; eens + Maak je geen zorgen, het is goed, ___. | hoor | toch; even; wel + ` + }), + lesson({ + id: 'nl-te', + title: 'Om te', + summary: 'Infinitive constructions with te and om … te', + notes: ` + ### te + infinitive + After many verbs the second verb takes **te**: *Ik probeer **te** slapen. Het begint **te** regenen. Hij vergeet **te** bellen.* + + No *te* after modal verbs (*kunnen, moeten, willen, mogen, zullen*) or *gaan, komen, blijven, laten, zien, horen*: + *Ik kan zwemmen. Ik ga slapen.* + + ### om … te — purpose, "in order to" + *Ik ga naar de winkel **om** brood **te** kopen.* — I'm going to the shop to buy bread. + + **Om te** is also used after adjectives and nouns: *Het is leuk **om te** doen. Ik heb geen tijd **om te** koken.* + + ### zitten/staan/liggen + te — ongoing action + *Hij **zit te** lezen.* — He's (sitting) reading. *Ze **staat te** wachten.* + Also: **aan het** + infinitive: *Ik ben **aan het** koken.* — I'm cooking. + `, + words: ` + proberen | to try + beginnen | to begin + vergeten | to forget + beloven | to promise + besluiten | to decide + weigeren | to refuse + hoeven | to need to (with niet) + durven | to dare + aan het … zijn | to be busy …ing + om … te | in order to + zonder … te | without …ing + in plaats van … te | instead of …ing + `, + sentences: ` + Ik probeer te slapen. | I'm trying to sleep. + Het begint te regenen. | It's starting to rain. + Ik ga naar de winkel om brood te kopen. | I'm going to the shop to buy bread. + Ik heb geen tijd om te koken. | I don't have time to cook. + Hij zit de hele dag te gamen. | He sits gaming all day. + We zijn aan het eten. | We're eating. + Hij vertrok zonder iets te zeggen. | He left without saying anything. + Ze besloot om te verhuizen. | She decided to move. + `, + drills: ` + Ik kan goed ___. (zwemmen) | zwemmen | te zwemmen; om te zwemmen; zwem | No te after modal verbs. + Hij vergat ___. (bellen) | te bellen | bellen; om bellen; gebeld + Ik spaar geld ___ een auto ___ kopen. | om … te | voor … te; om … –; te … om + Ze ___ te wachten op de bus. (standing) | staat | zit; ligt; is + Ik ben ___ koken. | aan het | om te; bezig; te + ` + }), + lesson({ + id: 'nl-nieuws', + title: 'Het nieuws', + summary: 'Society, politics and news vocabulary', + notes: ` + The Netherlands is a **constitutional monarchy** with a parliamentary democracy. + - **De Tweede Kamer** — House of Representatives (150 seats), the most important chamber. + - **Het kabinet** — the cabinet; **de minister-president** — the prime minister. + - Governments are always **coalitions** (*een coalitie*) of several parties, formed after long negotiations (*de formatie*). + + Reading tip: news headlines drop articles and verbs: *"Kabinet valt na ruzie over asiel"* — The cabinet falls after a row over asylum. + `, + words: ` + de regering | the government + het kabinet | the cabinet + de minister | the minister + de verkiezingen | the elections + stemmen | to vote + de partij | the (political) party + de coalitie | the coalition + de wet | the law + de belasting | the tax + de economie | the economy + de werkloosheid | unemployment + het onderzoek | the research; the investigation + volgens | according to + de maatschappij | society + het milieu | the environment + de klimaatverandering | climate change + de woningnood | the housing shortage + het protest | the protest + `, + sentences: ` + Er zijn volgend jaar verkiezingen. | There are elections next year. + Op welke partij ga je stemmen? | Which party are you going to vote for? + De regering wil de belasting verhogen. | The government wants to raise taxes. + Volgens onderzoek fietsen Nederlanders steeds meer. | According to research, Dutch people are cycling more and more. + De woningnood is een groot probleem. | The housing shortage is a big problem. + Het kabinet is gevallen. | The cabinet has fallen. + De werkloosheid daalt. | Unemployment is falling. + `, + drills: ` + Ik ga ___ op een kleine partij. (vote) | stemmen | kiezen; voten; stemmen voor + ___ de krant wordt het morgen warm. | Volgens | Door; Van; Na + De ___ is gevallen. (cabinet) | kabinet | kast; kabinetje; regeer + ` + }), + lesson({ + id: 'nl-uitdrukkingen', + title: 'Uitdrukkingen', + summary: 'Everyday Dutch idioms', + notes: ` + Dutch idioms are vivid, often about food, animals and the weather. Don't translate them literally! + + | idiom | literally | meaning | + |---|---|---| + | Het regent pijpenstelen. | It's raining pipe stems. | It's pouring. | + | Iemand in de maling nemen | to take someone in the grinder | to pull someone's leg | + | Met de deur in huis vallen | to fall into the house with the door | to get straight to the point | + | Nu komt de aap uit de mouw. | Now the monkey comes out of the sleeve. | Now the truth comes out. | + | Dat is een koud kunstje. | That's a cold trick. | That's a piece of cake. | + | Tot in de puntjes | to the little points | down to the last detail | + | Een appeltje te schillen hebben | to have an apple to peel | to have a bone to pick | + | Zo gek als een deur | as crazy as a door | completely mad | + | Voor een appel en een ei | for an apple and an egg | for next to nothing | + | De kat uit de boom kijken | to watch the cat out of the tree | to wait and see | + `, + words: ` + het regent pijpenstelen | it's raining cats and dogs + iemand in de maling nemen | to pull someone's leg + met de deur in huis vallen | to get straight to the point + nu komt de aap uit de mouw | now the truth comes out + een koud kunstje | a piece of cake + tot in de puntjes | down to the last detail + een appeltje te schillen hebben | to have a bone to pick + voor een appel en een ei | for next to nothing + de kat uit de boom kijken | to wait and see + zo gek als een deur | completely mad + ik heb er een hard hoofd in | I have my doubts about it + dat zit wel snor | that'll be fine + `, + sentences: ` + Ik val maar met de deur in huis: ik stop. | I'll get straight to the point: I'm quitting. + Je neemt me in de maling! | You're pulling my leg! + Ik heb die fiets voor een appel en een ei gekocht. | I bought that bike for next to nothing. + Ik heb nog een appeltje met je te schillen. | I have a bone to pick with you. + We kijken eerst de kat uit de boom. | We'll wait and see first. + Alles was tot in de puntjes geregeld. | Everything was organised down to the last detail. + `, + drills: ` + It's pouring: het regent ___ | pijpenstelen | katten en honden; emmers; bakstenen + To pull someone's leg: iemand in de ___ nemen | maling | molen; boot; war + For next to nothing: voor een appel en een ___ | ei | peer; banaan; brood + Now the truth comes out: nu komt de ___ uit de mouw | aap | kat; muis; hond + ` + }) + ] +); diff --git a/src/lib/language-learner/courses/nl/c1.ts b/src/lib/language-learner/courses/nl/c1.ts new file mode 100644 index 0000000..625211c --- /dev/null +++ b/src/lib/language-learner/courses/nl/c1.ts @@ -0,0 +1,318 @@ +import { lesson, level } from '../../course'; + +export default level( + 'c1', + 'C1', + 'Advanced', + 'Formal register, verb clusters, false friends, nuance and proverbs.', + [ + lesson({ + id: 'nl-formeel', + title: 'Formeel schrijven', + summary: 'Formal register and written connectors', + notes: ` + Formal written Dutch (*ambtelijke taal* when overdone) prefers longer connectors: + + | everyday | formal | meaning | + |---|---|---| + | maar | echter | however | + | omdat | aangezien | since, as | + | dus | derhalve / daarom | therefore | + | toch | desondanks / desalniettemin | nevertheless | + | over | betreffende / inzake | regarding | + | als | indien | if | + | krijgen | ontvangen | to receive | + | sturen | toezenden | to send | + + **Echter** is not used at the start of a sentence in good style: *Het plan is goed. Het is **echter** duur.* + + Formal letters: *Geachte heer/mevrouw …* — *Met vriendelijke groet*. + Emails between colleagues: *Beste Anna,* … *Groet,* / *Groetjes* (informal). + `, + words: ` + echter | however + aangezien | since; given that + derhalve | therefore + desondanks | nevertheless + indien | if (formal) + betreffende | regarding + ten aanzien van | with regard to + naar aanleiding van | following; in response to + in verband met | in connection with; due to + met betrekking tot | with respect to + hoogachtend | yours faithfully + geachte | dear (formal) + bijgevoegd | attached + in behandeling nemen | to process; to handle + `, + sentences: ` + Geachte mevrouw De Vries, | Dear Mrs De Vries, + Naar aanleiding van uw brief laat ik u het volgende weten. | In response to your letter, I would like to inform you of the following. + Het voorstel is interessant; het is echter te duur. | The proposal is interesting; however, it is too expensive. + Aangezien de termijn verlopen is, kunnen wij uw aanvraag niet in behandeling nemen. | Since the deadline has passed, we cannot process your application. + Indien u vragen heeft, kunt u contact met ons opnemen. | Should you have any questions, you can contact us. + In verband met ziekte is de winkel gesloten. | Due to illness, the shop is closed. + Bijgevoegd vindt u mijn cv. | Please find my CV attached. + `, + drills: ` + Formal "als": ___ u niet tevreden bent, … | Indien | Aangezien; Echter; Derhalve + Formal "omdat": ___ het regent, gaat het feest niet door. | Aangezien | Echter; Indien; Desondanks + Het was duur. Hij kocht het ___. (nevertheless) | desondanks | derhalve; aangezien; indien + ___ uw vraag van 3 mei … (in response to) | Naar aanleiding van | In verband met; Ten gevolge van; Ondanks + Formal close: Met vriendelijke ___ | groet | hoogachting; dank; wensen + ` + }), + lesson({ + id: 'nl-werkwoordclusters', + title: 'Heeft moeten doen', + summary: 'Verb clusters and the double infinitive', + notes: ` + When a modal (or *laten, gaan, blijven, zien, horen*) appears in the perfect, its participle becomes an **infinitive** (*infinitivus pro participio*): + + - *Ik **heb** moeten werken.* — not ~~gemoeten~~ + - *Hij **heeft** het niet kunnen vinden.* — He couldn't find it. + - *We **hebben** een taxi laten komen.* — We had a taxi come. + - *Ze **is** blijven slapen.* — She stayed over. + + In subordinate clauses, all verbs cluster at the end, usually with the finite verb first: + *…omdat ik **heb moeten werken**.* / *…dat hij het **had kunnen weten**.* + + Longer clusters are grammatical but heavy: + *Hij **zou** het **hebben kunnen doen**.* — He could have done it. + `, + words: ` + heb moeten | had to + heeft kunnen | was able to + had willen | would have wanted to + laten | to let; to have (something done) + blijven | to stay; to keep (doing) + zou hebben kunnen | could have + had moeten | should have + laten weten | to let know + laten zien | to show + leren kennen | to get to know + `, + sentences: ` + Ik heb gisteren moeten werken. | I had to work yesterday. + Hij heeft het niet kunnen vinden. | He couldn't find it. + We hebben een pizza laten bezorgen. | We had a pizza delivered. + Ze is bij ons blijven eten. | She stayed for dinner at our place. + Je had het me moeten vertellen. | You should have told me. + Ik weet dat hij het had kunnen weten. | I know he could have known. + Waar heb je haar leren kennen? | Where did you get to know her? + Laat me even weten of je komt. | Just let me know if you're coming. + `, + drills: ` + Ik heb hard ___ werken. | moeten | gemoeten; moet; te moeten | Double infinitive: no ge-. + Hij heeft niet ___ komen. | kunnen | gekund; kan; te kunnen + We hebben de auto ___ repareren. (had it repaired) | laten | gelaten; laat; te laten + Je ___ het me moeten zeggen. (should have) | had | hebt; zou; was + …omdat ik gisteren ___. | heb moeten werken | moeten heb werken; werken heb gemoeten; heb gemoeten werken + ` + }), + lesson({ + id: 'nl-valse-vrienden', + title: 'Valse vrienden', + summary: 'False friends between Dutch and English', + notes: ` + These look like English words but mean something else: + + | Dutch | means | not | + |---|---|---| + | eventueel | possibly, if necessary | eventually (= uiteindelijk) | + | actueel | current, topical | actual (= werkelijk) | + | brutaal | cheeky, rude | brutal (= wreed) | + | sympathiek | likeable, nice | sympathetic (= meelevend) | + | slim | clever | slim (= slank) | + | bellen | to call (phone) | to ring a bell only | + | bekomen | to recover (from a shock) | become (= worden) | + | door | through; by | door (= deur) | + | raar | strange | rare (= zeldzaam) | + | lust | to like (food) | lust (= begeerte) | + | winkel | shop | winkle | + | wandelen | to walk, stroll | to wander (= dwalen) | + | monster | sample (also: monster) | | + `, + words: ` + eventueel | possibly; if necessary + uiteindelijk | eventually; in the end + actueel | current; topical + werkelijk | actual; real + brutaal | cheeky; rude + wreed | cruel; brutal + sympathiek | likeable; nice + slim | clever + slank | slim + raar | strange; weird + zeldzaam | rare + wandelen | to walk; to stroll + het monster | the sample; the monster + bekomen | to recover (from a shock) + `, + sentences: ` + Eventueel kunnen we morgen afspreken. | If need be, we can meet tomorrow. + Uiteindelijk heeft hij toch gewonnen. | In the end he won after all. + Dat is een heel actueel onderwerp. | That's a very topical subject. + Wat een brutaal kind! | What a cheeky child! + Ze is heel sympathiek. | She's very likeable. + Hij is erg slim. | He's very clever. + Wat een raar verhaal. | What a strange story. + Ik moet even bekomen van de schrik. | I need a moment to recover from the shock. + `, + drills: ` + "Eventually" in Dutch: ___ | uiteindelijk | eventueel; actueel; eindelijk + "Actual" in Dutch: ___ | werkelijk | actueel; eventueel; actief + Hij is ___: hij haalt altijd een tien. (clever) | slim | slank; sympathiek; brutaal + "Rare" (uncommon) in Dutch: ___ | zeldzaam | raar; rauw; weinig raar + Een ___ kind geeft een grote mond. (cheeky) | brutaal | wreed; sympathiek; slim + ` + }), + lesson({ + id: 'nl-abstract', + title: 'Abstracte begrippen', + summary: 'Abstract vocabulary for discussion and debate', + notes: ` + Nouns ending in **-ing** are de-words and usually come from verbs: *ontwikkelen → de ontwikkeling*, *veranderen → de verandering*. + Nouns ending in **-heid** are de-words from adjectives: *vrij → de vrijheid*, *gezond → de gezondheid*. + Nouns with **ge-…-te** are het-words and collective: *het gebergte*, *het gebladerte*. + + Useful debate phrases: + - *Enerzijds … anderzijds …* — on the one hand … on the other hand … + - *Het valt niet te ontkennen dat …* — It can't be denied that … + - *Dat hangt af van …* — That depends on … + - *Kortom, …* — In short, … + `, + words: ` + de ontwikkeling | the development + de verandering | the change + de vrijheid | the freedom + de verantwoordelijkheid | the responsibility + de gelijkheid | equality + de invloed | the influence + het gevolg | the consequence + de oorzaak | the cause + de uitdaging | the challenge + het beleid | the policy + de samenleving | society + duurzaam | sustainable + enerzijds | on the one hand + anderzijds | on the other hand + kortom | in short + ontkennen | to deny + benadrukken | to emphasise + aantonen | to demonstrate + `, + sentences: ` + Enerzijds is het goedkoop, anderzijds is het slecht voor het milieu. | On the one hand it's cheap, on the other hand it's bad for the environment. + Het valt niet te ontkennen dat er een probleem is. | It can't be denied that there's a problem. + Dat heeft grote gevolgen voor de samenleving. | That has major consequences for society. + Wat is de oorzaak van het probleem? | What is the cause of the problem? + Het onderzoek toont aan dat het werkt. | The research shows that it works. + Kortom, we moeten ons beleid veranderen. | In short, we must change our policy. + Iedereen draagt verantwoordelijkheid. | Everyone bears responsibility. + `, + drills: ` + ___ ontwikkeling | de | het | -ing nouns are de-words. + ___ vrijheid | de | het | -heid nouns are de-words. + Enerzijds is het goed, ___ is het duur. | anderzijds | enerzijds; bovendien; kortom + Het onderzoek ___ aan dat het werkt. | toont | laat; wijst; geeft | Aantonen is separable. + ` + }), + lesson({ + id: 'nl-collocaties', + title: 'Vaste combinaties', + summary: 'Collocations: which verb goes with which noun', + notes: ` + Many nouns take a fixed verb. Translating the English verb often goes wrong: + + | Dutch | English | + |---|---| + | een besluit **nemen** | to make a decision | + | een foto **maken** | to take a photo | + | een vraag **stellen** | to ask a question | + | rekening **houden** met | to take into account | + | een afspraak **maken** | to make an appointment | + | aandacht **besteden** aan | to pay attention to | + | een rol **spelen** | to play a role | + | een fout **maken** | to make a mistake | + | contact **opnemen** met | to get in touch with | + | een beslissing **nemen** | to take a decision | + | indruk **maken** op | to impress | + | ontslag **nemen** | to resign | + | moeite **doen** | to make an effort | + `, + words: ` + een besluit nemen | to make a decision + een vraag stellen | to ask a question + rekening houden met | to take into account + aandacht besteden aan | to pay attention to + een rol spelen | to play a role + contact opnemen met | to get in touch with + indruk maken op | to impress + ontslag nemen | to resign + moeite doen | to make an effort + in aanmerking komen voor | to be eligible for + ter sprake komen | to come up (in conversation) + op de hoogte houden | to keep informed + `, + sentences: ` + We moeten vandaag een besluit nemen. | We have to make a decision today. + Mag ik een vraag stellen? | May I ask a question? + Houd rekening met vertraging. | Take delays into account. + Geld speelt geen rol. | Money is no object. + Ik neem morgen contact met u op. | I'll get in touch with you tomorrow. + Hij heeft ontslag genomen. | He has resigned. + Houd me op de hoogte! | Keep me posted! + Dat onderwerp kwam niet ter sprake. | That subject didn't come up. + `, + drills: ` + een vraag ___ (to ask) | stellen | vragen; maken; doen + een besluit ___ (to make) | nemen | maken; doen; stellen + een foto ___ (to take) | maken | nemen; schieten; doen + ___ houden met (take into account) | rekening | aandacht; indruk; moeite + Hij heeft veel indruk op me ___. | gemaakt | gedaan; genomen; gelaten + ` + }), + lesson({ + id: 'nl-spreekwoorden', + title: 'Spreekwoorden', + summary: 'Proverbs and sayings', + notes: ` + | proverb | meaning | + |---|---| + | Wie niet waagt, wie niet wint. | Nothing ventured, nothing gained. | + | Oost west, thuis best. | There's no place like home. | + | Beter een vogel in de hand dan tien in de lucht. | A bird in the hand is worth two in the bush. | + | Doe maar normaal, dan doe je al gek genoeg. | Just act normal, that's crazy enough. (very Dutch!) | + | Wie het laatst lacht, lacht het best. | He who laughs last laughs best. | + | Geen woorden maar daden. | Actions speak louder than words. | + | Zo vader, zo zoon. | Like father, like son. | + | Als de kat van huis is, dansen de muizen op tafel. | When the cat's away, the mice will play. | + | Hoge bomen vangen veel wind. | Tall trees catch a lot of wind (prominence invites criticism). | + | Een ezel stoot zich in het gemeen niet twee keer aan dezelfde steen. | A donkey doesn't hit the same stone twice. | + `, + words: ` + wie niet waagt, wie niet wint | nothing ventured, nothing gained + oost west, thuis best | there's no place like home + geen woorden maar daden | actions speak louder than words + zo vader, zo zoon | like father, like son + doe maar normaal | just act normal + hoge bomen vangen veel wind | tall trees catch a lot of wind + wie het laatst lacht, lacht het best | he who laughs last laughs best + het spreekwoord | the proverb + `, + sentences: ` + Doe maar normaal, dan doe je al gek genoeg. | Just act normal, that's crazy enough. + Beter een vogel in de hand dan tien in de lucht. | A bird in the hand is worth two in the bush. + Als de kat van huis is, dansen de muizen op tafel. | When the cat's away, the mice will play. + Na de vakantie dacht ik: oost west, thuis best. | After the holiday I thought: there's no place like home. + `, + drills: ` + Wie niet waagt, wie niet ___ | wint | verliest; wacht; weet + Oost west, thuis ___ | best | rust; goed; mooi + Beter een vogel in de hand dan tien in de ___ | lucht | boom; tuin; wolken + Als de kat van huis is, dansen de ___ op tafel | muizen | honden; ratten; vogels + ` + }) + ] +); diff --git a/src/lib/language-learner/courses/nl/index.ts b/src/lib/language-learner/courses/nl/index.ts new file mode 100644 index 0000000..6105765 --- /dev/null +++ b/src/lib/language-learner/courses/nl/index.ts @@ -0,0 +1,18 @@ +import { course } from '../../course'; +import a1 from './a1'; +import a2 from './a2'; +import b1 from './b1'; +import b2 from './b2'; +import c1 from './c1'; + +export default course( + { + code: 'nl', + name: 'Dutch', + nativeName: 'Nederlands', + glossLang: 'en', + voice: 'nl-NL', + tagline: 'From "hallo" to reading the news, taught in English.' + }, + [a1, a2, b1, b2, c1] +); diff --git a/src/lib/language-learner/drills/en.ts b/src/lib/language-learner/drills/en.ts new file mode 100644 index 0000000..321f24b --- /dev/null +++ b/src/lib/language-learner/drills/en.ts @@ -0,0 +1,455 @@ +import { englishNumber } from '../numbers'; +import { int, pick } from '../random'; +import { ask, type DrillMode } from './shared'; + +type Irregular = { + base: string; + past: string[]; + participle: string[]; + gloss: string; + level: number; +}; + +const irregulars = (level: number, source: string): Irregular[] => + source + .trim() + .split('\n') + .map((line) => { + const [base, past, participle, gloss] = line.split('|').map((p) => p.trim()); + return { base, past: past.split(';'), participle: participle.split(';'), gloss, level }; + }); + +export const IRREGULAR_VERBS: Irregular[] = [ + ...irregulars( + 1, + ` +be|was;were|been|zijn +have|had|had|hebben +do|did|done|doen +go|went|gone|gaan +come|came|come|komen +see|saw|seen|zien +eat|ate|eaten|eten +drink|drank|drunk|drinken +make|made|made|maken +get|got|got;gotten|krijgen, worden +give|gave|given|geven +take|took|taken|nemen +say|said|said|zeggen +know|knew|known|weten, kennen +think|thought|thought|denken +buy|bought|bought|kopen +read|read|read|lezen +write|wrote|written|schrijven +speak|spoke|spoken|spreken +sleep|slept|slept|slapen +` + ), + ...irregulars( + 2, + ` +begin|began|begun|beginnen +bring|brought|brought|brengen +build|built|built|bouwen +catch|caught|caught|vangen +choose|chose|chosen|kiezen +drive|drove|driven|rijden +fall|fell|fallen|vallen +feel|felt|felt|voelen +find|found|found|vinden +fly|flew|flown|vliegen +forget|forgot|forgotten|vergeten +hear|heard|heard|horen +keep|kept|kept|houden +leave|left|left|vertrekken, verlaten +lose|lost|lost|verliezen +meet|met|met|ontmoeten +pay|paid|paid|betalen +put|put|put|leggen, zetten +run|ran|run|rennen +sell|sold|sold|verkopen +send|sent|sent|sturen +sing|sang|sung|zingen +sit|sat|sat|zitten +stand|stood|stood|staan +swim|swam|swum|zwemmen +teach|taught|taught|onderwijzen +tell|told|told|vertellen +understand|understood|understood|begrijpen +wear|wore|worn|dragen (kleding) +win|won|won|winnen +` + ), + ...irregulars( + 3, + ` +bear|bore|borne;born|dragen, verdragen +beat|beat|beaten|verslaan +bend|bent|bent|buigen +bet|bet|bet|wedden +bite|bit|bitten|bijten +blow|blew|blown|blazen +break|broke|broken|breken +feed|fed|fed|voeren +fight|fought|fought|vechten +forgive|forgave|forgiven|vergeven +freeze|froze|frozen|bevriezen +grow|grew|grown|groeien +hang|hung|hung|hangen +hide|hid|hidden|verstoppen +hold|held|held|vasthouden +hurt|hurt|hurt|pijn doen +lay|laid|laid|leggen +lead|led|led|leiden +lend|lent|lent|uitlenen +lie|lay|lain|liggen +ride|rode|ridden|rijden (fiets, paard) +ring|rang|rung|bellen, luiden +rise|rose|risen|opstaan, stijgen +seek|sought|sought|zoeken +shake|shook|shaken|schudden +shine|shone|shone|schijnen +shoot|shot|shot|schieten +shut|shut|shut|sluiten +sink|sank|sunk|zinken +spend|spent|spent|uitgeven, doorbrengen +spread|spread|spread|verspreiden +steal|stole|stolen|stelen +stick|stuck|stuck|plakken +strike|struck|struck|slaan, staken +swear|swore|sworn|zweren, vloeken +tear|tore|torn|scheuren +throw|threw|thrown|gooien +wake|woke|woken|wakker worden +` + ), + ...irregulars( + 4, + ` +arise|arose|arisen|ontstaan +bind|bound|bound|binden +breed|bred|bred|fokken +cling|clung|clung|zich vastklampen +creep|crept|crept|kruipen +dwell|dwelt;dwelled|dwelt;dwelled|verblijven +flee|fled|fled|vluchten +forbid|forbade|forbidden|verbieden +foresee|foresaw|foreseen|voorzien +grind|ground|ground|malen +kneel|knelt;kneeled|knelt;kneeled|knielen +leap|leapt;leaped|leapt;leaped|springen +mislead|misled|misled|misleiden +overcome|overcame|overcome|overwinnen +sew|sewed|sewn;sewed|naaien +shrink|shrank|shrunk|krimpen +slide|slid|slid|glijden +sting|stung|stung|steken (insect) +stride|strode|stridden|schrijden +strive|strove;strived|striven;strived|streven +swell|swelled|swollen;swelled|zwellen +tread|trod|trodden|betreden +undertake|undertook|undertaken|ondernemen +weave|wove|woven|weven +withdraw|withdrew|withdrawn|terugtrekken +wring|wrung|wrung|uitwringen +` + ) +]; + +type Comparison = { + adj: string; + comparative: string[]; + superlative: string[]; + gloss: string; + level: number; +}; + +const comparisons = (level: number, source: string): Comparison[] => + source + .trim() + .split('\n') + .map((line) => { + const [adj, comparative, superlative, gloss] = line.split('|').map((p) => p.trim()); + return { + adj, + comparative: comparative.split(';'), + superlative: superlative.split(';'), + gloss, + level + }; + }); + +const COMPARISONS: Comparison[] = [ + ...comparisons( + 1, + ` +big|bigger|biggest|groot +small|smaller|smallest|klein +tall|taller|tallest|lang (persoon) +old|older|oldest|oud +young|younger|youngest|jong +long|longer|longest|lang +fast|faster|fastest|snel +cheap|cheaper|cheapest|goedkoop +hot|hotter|hottest|heet +happy|happier|happiest|blij +easy|easier|easiest|makkelijk +good|better|best|goed +bad|worse|worst|slecht +nice|nicer|nicest|leuk, aardig +beautiful|more beautiful|most beautiful|mooi +expensive|more expensive|most expensive|duur +` + ), + ...comparisons( + 2, + ` +busy|busier|busiest|druk +funny|funnier|funniest|grappig +heavy|heavier|heaviest|zwaar +early|earlier|earliest|vroeg +far|farther;further|farthest;furthest|ver +little|less|least|weinig +much|more|most|veel +thin|thinner|thinnest|dun +wet|wetter|wettest|nat +sad|sadder|saddest|verdrietig +large|larger|largest|groot +safe|safer|safest|veilig +simple|simpler;more simple|simplest;most simple|eenvoudig +quiet|quieter|quietest|stil +interesting|more interesting|most interesting|interessant +important|more important|most important|belangrijk +difficult|more difficult|most difficult|moeilijk +dangerous|more dangerous|most dangerous|gevaarlijk +comfortable|more comfortable|most comfortable|comfortabel +famous|more famous|most famous|beroemd +careful|more careful|most careful|voorzichtig +boring|more boring|most boring|saai +modern|more modern|most modern|modern +` + ) +]; + +type Article = { article: 'a' | 'an'; word: string; gloss: string; why?: string }; + +const ARTICLES: Article[] = ` +an|apple|appel +a|banana|banaan +an|egg|ei +an|hour|uur|The h is silent: "an our". +a|house|huis +a|university|universiteit|"University" starts with a "y" sound: "a you-niversity". +an|umbrella|paraplu +a|European|Europeaan|"European" starts with a "y" sound. +an|honest man|eerlijke man|The h is silent in "honest". +a|one-way ticket|enkeltje|"One" starts with a "w" sound. +an|MP|parlementslid|You say "em-pee", which starts with a vowel sound. +a|UFO|ufo|You say "you-eff-oh". +an|idea|idee +a|year|jaar +an|X-ray|röntgenfoto|You say "eks-ray". +a|unicorn|eenhoorn +an|heir|erfgenaam|The h is silent in "heir". +a|uniform|uniform +an|orange|sinaasappel +a|dog|hond +an|elephant|olifant +a|user|gebruiker +an|ice cream|ijsje +a|euro|euro +an|owl|uil +a|hotel|hotel +an|FBI agent|FBI-agent|You say "eff-bee-eye". +a|useful tip|nuttige tip +an|interesting book|interessant boek +a|horrible day|vreselijke dag +an|hourly rate|uurtarief +a|history lesson|geschiedenisles +an|old friend|oude vriend +a|yellow car|gele auto +an|unusual name|ongewone naam +a|one-euro coin|munt van één euro +` + .trim() + .split('\n') + .map((line) => { + const [article, word, gloss, why] = line.split('|'); + return { article: article as 'a' | 'an', word, gloss, why }; + }); + +const TIME_WORDS = [ + 'twelve', + 'one', + 'two', + 'three', + 'four', + 'five', + 'six', + 'seven', + 'eight', + 'nine', + 'ten', + 'eleven', + 'twelve' +]; + +export function englishTime(hour: number, minute: number): string[] { + const h = TIME_WORDS[hour % 12 || 12]; + const next = TIME_WORDS[(hour % 12) + 1]; + const digital = + minute === 0 + ? `${h} o'clock` + : `${h} ${minute < 10 ? 'oh ' + englishNumber(minute) : englishNumber(minute)}`; + if (minute === 0) return [digital]; + if (minute === 15) return [`quarter past ${h}`, `a quarter past ${h}`, digital]; + if (minute === 30) return [`half past ${h}`, digital]; + if (minute === 45) return [`quarter to ${next}`, `a quarter to ${next}`, digital]; + if (minute % 5 === 0) { + const m = minute < 30 ? minute : 60 - minute; + const word = englishNumber(m); + return [minute < 30 ? `${word} past ${h}` : `${word} to ${next}`, digital]; + } + return [digital]; +} + +export const drills: DrillMode[] = [ + { + id: 'irregular', + title: 'Irregular verbs', + description: + 'Past simple and past participle of the irregular verbs, from "go, went, gone" upwards.', + make: (ctx) => { + const verb = pick( + ctx.rng, + IRREGULAR_VERBS.filter((v) => v.level <= ctx.level) + ); + const participle = ctx.rng() < 0.5; + const answer = participle ? verb.participle : verb.past; + const b = verb.base; + const regular = b.endsWith('e') + ? b + 'd' + : b.endsWith('y') + ? b.slice(0, -1) + 'ied' + : b + 'ed'; + return ask(ctx, { + skill: participle ? 'irregular-participle' : 'irregular-past', + instruction: participle ? 'Past participle (I have …)' : 'Past simple (yesterday I …)', + prompt: b, + promptLang: 'target', + context: verb.gloss, + answer, + wrong: [ + regular, + ...(participle ? verb.past : verb.participle), + b, + b + 's', + regular.replace(/ed$/, 'en') + ], + audio: answer[0] + }); + } + }, + { + id: 'comparatives', + title: 'Comparatives', + description: 'bigger, the biggest, more beautiful, better, worst: -er/-est or more/most.', + make: (ctx) => { + const adj = pick( + ctx.rng, + COMPARISONS.filter((c) => c.level <= ctx.level) + ); + const superlative = ctx.rng() < 0.5; + const answer = superlative ? adj.superlative : adj.comparative; + const a = adj.adj; + return ask(ctx, { + skill: superlative ? 'comparatives-superlative' : 'comparatives-comparative', + instruction: superlative ? 'Superlative (the … -est)' : 'Comparative (… -er than)', + prompt: a, + promptLang: 'target', + context: adj.gloss, + answer: superlative ? answer.flatMap((s) => [s, `the ${s}`]) : answer, + wrong: superlative + ? [a + 'est', `most ${a}`, `${a}est`.replace(/yest$/, 'iest'), adj.comparative[0]] + : [ + a + 'er', + `more ${a}`, + a.replace(/([^aeiou])$/, '$1$1') + 'er', + `${a}er`.replace(/yer$/, 'ier') + ], + explain: + a.length > 6 && !/y$/.test(a) + ? 'Long adjectives use more/most instead of -er/-est.' + : undefined + }); + } + }, + { + id: 'articles', + title: 'A or an', + description: 'It is about the sound, not the letter: an hour, a university.', + make: (ctx) => { + const item = pick(ctx.rng, ARTICLES); + return ask(ctx, { + skill: 'articles', + instruction: 'A or an?', + prompt: `___ ${item.word}`, + promptLang: 'target', + context: item.gloss, + answer: [item.article], + wrong: [item.article === 'a' ? 'an' : 'a'], + choice: true, + audio: `${item.article} ${item.word}`, + explain: + item.why ?? + `"${item.word}" starts with a ${item.article === 'an' ? 'vowel' : 'consonant'} sound.` + }); + } + }, + { + id: 'numbers', + title: 'Numbers', + description: 'Write numbers in words, from seven to seven million.', + make: (ctx) => { + const max = [20, 100, 1000, 10000, 5_000_000][Math.min(ctx.level, 5) - 1]; + const n = int(ctx.rng, 0, max); + const answer = englishNumber(n); + return ask(ctx, { + skill: 'numbers', + instruction: 'Write this number in English', + prompt: n.toLocaleString('en-GB'), + answer: [answer, answer.replace(/ and /g, ' ')], + wrong: [n + 1, Math.max(0, n - 1), n + 10, n * 10] + .filter((m) => m !== n) + .map(englishNumber), + audio: answer, + explain: + n > 100 ? 'British English adds "and" before the tens: one hundred and five.' : undefined + }); + } + }, + { + id: 'clock', + title: 'Telling time', + description: '"Half past two" is 2:30, not half drie. Practise both styles.', + make: (ctx) => { + const hour = int(ctx.rng, 1, 12); + const minute = ctx.level <= 1 ? pick(ctx.rng, [0, 15, 30, 45]) : int(ctx.rng, 0, 11) * 5; + const answer = englishTime(hour, minute); + const shift = (h: number) => ((h + 11) % 12) + 1; + return ask(ctx, { + skill: 'clock', + instruction: 'How do you say this time in English?', + prompt: `${hour}:${String(minute).padStart(2, '0')}`, + answer, + wrong: [ + englishTime(shift(hour + 1), minute)[0], + englishTime(shift(hour - 1), minute)[0], + englishTime(hour, (minute + 30) % 60)[0] + ], + audio: answer[0], + explain: minute === 30 ? 'Dutch "half drie" is English "half past two".' : undefined + }); + } + } +]; diff --git a/src/lib/language-learner/drills/index.ts b/src/lib/language-learner/drills/index.ts new file mode 100644 index 0000000..4d597c4 --- /dev/null +++ b/src/lib/language-learner/drills/index.ts @@ -0,0 +1,14 @@ +import type { LangCode } from '../types'; +import type { DrillMode } from './shared'; + +export type { DrillMode, DrillContext } from './shared'; + +const loaders: Record Promise<{ drills: DrillMode[] }>> = { + nl: () => import('./nl'), + ko: () => import('./ko'), + en: () => import('./en') +}; + +export async function loadDrills(code: LangCode): Promise { + return (await loaders[code]()).drills; +} diff --git a/src/lib/language-learner/drills/ko.ts b/src/lib/language-learner/drills/ko.ts new file mode 100644 index 0000000..6aca577 --- /dev/null +++ b/src/lib/language-learner/drills/ko.ts @@ -0,0 +1,382 @@ +import { compose, FINALS, lastSyllable, particle, romanize, type Particle } from '../hangul'; +import { + ACTION_FORMS, + conjugate, + DESCRIPTIVE_FORMS, + FORM_LABELS, + infinitive, + KOREAN_VERBS, + type KoreanVerb +} from '../korean-verbs'; +import { nativeKorean, sinoKorean } from '../numbers'; +import { int, pick, sample } from '../random'; +import { ask, withAndWithoutSpaces, type DrillContext, type DrillMode } from './shared'; + +const BASIC_CONSONANTS = [...'ㄱㄴㄷㄹㅁㅂㅅㅇㅈㅎ']; +const EXTRA_CONSONANTS = [...'ㅋㅌㅍㅊㄲㄸㅃㅆㅉ']; +const BASIC_VOWELS = [...'ㅏㅓㅗㅜㅡㅣ']; +const EXTRA_VOWELS = [...'ㅐㅔㅑㅕㅛㅠㅘㅝㅚㅟㅢㅙㅞㅒㅖ']; +const SIMPLE_FINALS = [...'ㄱㄴㄷㄹㅁㅂㅇ']; +const MORE_FINALS = [...'ㅅㅈㅊㅋㅌㅍㅎㄲㅆ']; + +function randomSyllable(ctx: DrillContext): string { + const { rng, level } = ctx; + const initials = level >= 2 ? [...BASIC_CONSONANTS, ...EXTRA_CONSONANTS] : BASIC_CONSONANTS; + const vowels = level >= 3 ? [...BASIC_VOWELS, ...EXTRA_VOWELS] : BASIC_VOWELS; + const finals = level >= 5 ? [...SIMPLE_FINALS, ...MORE_FINALS] : SIMPLE_FINALS; + const final = level >= 4 && rng() < 0.6 ? pick(rng, finals) : ''; + return compose({ + initial: pick(rng, initials), + medial: pick(rng, vowels), + final: FINALS.includes(final) ? final : '' + }); +} + +type Noun = { word: string; gloss: string }; + +const NOUNS: Noun[] = ` +학생|student +선생님|teacher +친구|friend +책|book +사과|apple +물|water +커피|coffee +우유|milk +가방|bag +집|house +학교|school +회사|company +병원|hospital +은행|bank +시장|market +서울|Seoul +부산|Busan +한국|Korea +영어|English +한국어|Korean (language) +음식|food +밥|rice, meal +김치|kimchi +빵|bread +차|car, tea +버스|bus +지하철|subway +비행기|airplane +컴퓨터|computer +사람|person +이름|name +가족|family +어머니|mother +아버지|father +동생|younger sibling +개|dog +고양이|cat +날씨|weather +영화|movie +음악|music +노래|song +사진|photo +돈|money +시간|time +옷|clothes +신발|shoes +모자|hat +연필|pencil +의자|chair +문|door +방|room +화장실|bathroom +도서관|library +공원|park +바다|sea +산|mountain +편지|letter +선물|present +생일|birthday +지갑|wallet +` + .trim() + .split('\n') + .map((line) => { + const [word, gloss] = line.split('|'); + return { word, gloss }; + }); + +const PARTICLES: { kind: Particle; label: string; level: number; pair: [string, string] }[] = [ + { kind: 'topic', label: 'topic (은/는)', level: 1, pair: ['은', '는'] }, + { kind: 'subject', label: 'subject (이/가)', level: 1, pair: ['이', '가'] }, + { kind: 'object', label: 'object (을/를)', level: 1, pair: ['을', '를'] }, + { kind: 'copula', label: '"it is" (이에요/예요)', level: 1, pair: ['이에요', '예요'] }, + { kind: 'and', label: '"and" (과/와)', level: 2, pair: ['과', '와'] }, + { kind: 'with', label: '"with/and", casual (이랑/랑)', level: 2, pair: ['이랑', '랑'] }, + { kind: 'direction', label: '"towards/by" ((으)로)', level: 3, pair: ['으로', '로'] } +]; + +/** Mistakes a learner is likely to make: ignoring an irregular, or the wrong vowel. */ +function verbMistakes(verb: KoreanVerb, form: (typeof ACTION_FORMS)[number]): string[] { + const stem = verb.dict.slice(0, -1); + const variants: KoreanVerb[] = [ + { ...verb, irr: undefined }, + ...(['b', 'd', 'reu', 's', 'h'] as const) + .filter((i) => i !== verb.irr) + .map((irr) => ({ ...verb, irr })) + ]; + const wrong = variants.flatMap((v) => { + try { + return conjugate(v, form); + } catch { + return []; + } + }); + const inf = infinitive(verb); + const flipped = inf.endsWith('아') + ? inf.slice(0, -1) + '어' + : inf.endsWith('어') + ? inf.slice(0, -1) + '아' + : inf; + if (form === 'present') wrong.push(flipped + '요', stem + '어요', stem + '아요'); + if (form === 'past') + wrong.push( + stem + '었어요', + stem + '았어요', + flipped.replace(/[아어]$/, (v) => (v === '아' ? '았' : '었')) + '어요' + ); + if (form === 'formal') wrong.push(stem + '습니다', stem + 'ㅂ니다', stem + '니다'); + if (form === 'future') wrong.push(stem + '을 거예요', stem + 'ㄹ 거예요', stem + ' 거예요'); + const others = (verb.action ? ACTION_FORMS : DESCRIPTIVE_FORMS).filter((f) => f !== form); + return [...wrong, ...others.map((f) => conjugate(verb, f)[0])]; +} + +type CountTask = { prompt: string; answer: string[]; wrong: string[]; explain: string }; + +function countTask(ctx: DrillContext): CountTask { + const { rng, level } = ctx; + const tasks: (() => CountTask)[] = [ + () => { + const n = int(rng, 1, level >= 3 ? 99999 : level >= 2 ? 1000 : 100); + return { + prompt: `${n.toLocaleString('en-US')} (Sino-Korean)`, + answer: [sinoKorean(n)], + wrong: [ + sinoKorean(n + 1), + sinoKorean(Math.max(1, n - 1)), + sinoKorean(n * 10), + sinoKorean(n + 10) + ], + explain: 'Sino-Korean: 일 이 삼 사 오 육 칠 팔 구 십, 백 (100), 천 (1,000), 만 (10,000).' + }; + }, + () => { + const n = int(rng, 1, level >= 2 ? 99 : 10); + return { + prompt: `${n} (native Korean)`, + answer: [nativeKorean(n)], + wrong: [ + nativeKorean(n === 99 ? 98 : n + 1), + sinoKorean(n), + nativeKorean(n, true), + nativeKorean(n > 1 ? n - 1 : 2) + ], + explain: + 'Native: 하나 둘 셋 넷 다섯 여섯 일곱 여덟 아홉 열, then 스물 서른 마흔 쉰 예순 일흔 여든 아흔.' + }; + }, + () => { + const [counter, gloss] = pick(rng, [ + ['명', 'people'], + ['개', 'things'], + ['살', 'years old'], + ['잔', 'cups'], + ['병', 'bottles'], + ['마리', 'animals'], + ['권', 'books'] + ] as const); + const n = int(rng, 1, level >= 2 ? 40 : 10); + return { + prompt: `${n} ${gloss} (${counter})`, + answer: withAndWithoutSpaces([`${nativeKorean(n, true)} ${counter}`]), + wrong: [ + `${nativeKorean(n)} ${counter}`, + `${sinoKorean(n)} ${counter}`, + `${nativeKorean(n === 40 ? 39 : n + 1, true)} ${counter}` + ], + explain: + 'Counting things uses native numbers, shortened before a counter: 한, 두, 세, 네, 스무.' + }; + }, + () => { + const hour = int(rng, 1, 12); + const minute = level >= 2 ? int(rng, 0, 11) * 5 : pick(rng, [0, 30]); + const h = `${nativeKorean(hour, true)} 시`; + const answer = + minute === 0 + ? [h] + : minute === 30 + ? [`${h} 반`, `${h} ${sinoKorean(30)} 분`] + : [`${h} ${sinoKorean(minute)} 분`]; + return { + prompt: `${hour}:${String(minute).padStart(2, '0')}`, + answer: withAndWithoutSpaces(answer), + wrong: [ + `${sinoKorean(hour)} 시${minute ? ` ${sinoKorean(minute)} 분` : ''}`, + `${nativeKorean(hour, true)} 시${minute ? ` ${nativeKorean(Math.min(minute, 99), true)} 분` : ''}`, + `${nativeKorean(hour === 12 ? 1 : hour + 1, true)} 시${minute ? ` ${sinoKorean(minute)} 분` : ''}` + ], + explain: + 'Hours use native numbers (한 시, 두 시), minutes use Sino-Korean (십 분). 반 = half past.' + }; + }, + () => { + const price = int(rng, 1, level >= 2 ? 500 : 50) * 100; + return { + prompt: `₩${price.toLocaleString('en-US')}`, + answer: withAndWithoutSpaces([`${sinoKorean(price)} 원`]), + wrong: [ + `${sinoKorean(price * 10)} 원`, + `${sinoKorean(Math.max(100, price - 100))} 원`, + `${sinoKorean(price + 1000)} 원` + ], + explain: 'Money uses Sino-Korean numbers. 10,000 is 만, so 50,000 is 오만.' + }; + }, + () => { + const month = int(rng, 1, 12); + const name = (m: number) => (m === 6 ? '유월' : m === 10 ? '시월' : `${sinoKorean(m)}월`); + const english = new Date(2026, month - 1, 1).toLocaleString('en-US', { month: 'long' }); + return { + prompt: english, + answer: [name(month)], + wrong: [ + `${sinoKorean(month)}월`, + `${nativeKorean(month, true)} 월`, + name((month % 12) + 1) + ], + explain: 'Months are Sino-Korean + 월. June (유월) and October (시월) drop a consonant.' + }; + } + ]; + return pick(rng, tasks)(); +} + +export const drills: DrillMode[] = [ + { + id: 'hangul', + title: 'Hangul reading', + description: + 'Read syllables and real words. Higher levels add tense consonants, compound vowels and batchim.', + make: (ctx) => { + const words = ctx.course.levels + .slice(1, 1 + ctx.level) + .flatMap((lv) => lv.lessons.flatMap((ls) => ls.words)) + .filter((w) => w.kind === 'word' && /^[가-힣]+$/.test(w.target[0])); + const useWord = ctx.level >= 3 && words.length && ctx.rng() < 0.5; + const text = useWord ? pick(ctx.rng, words).target[0] : randomSyllable(ctx); + const answer = romanize(text); + if (ctx.rng() < 0.5 || useWord) { + const others = Array.from({ length: 8 }, () => + romanize(useWord ? pick(ctx.rng, words).target[0] : randomSyllable(ctx)) + ); + return ask(ctx, { + skill: 'hangul', + instruction: 'How is this read?', + prompt: text, + promptLang: 'target', + answer: [answer], + wrong: others, + audio: text, + explain: useWord ? 'Romanization shows sound changes between syllables.' : undefined + }); + } + const others = Array.from({ length: 12 }, () => randomSyllable(ctx)).filter( + (s) => romanize(s) !== answer + ); + return ask(ctx, { + skill: 'hangul-write', + instruction: 'Which syllable is this?', + prompt: answer, + answer: [text], + wrong: others, + choice: true, + audio: text + }); + } + }, + { + id: 'particles', + title: 'Particles', + description: '은/는, 이/가, 을/를 and friends: pick the form that fits the noun.', + make: (ctx) => { + const noun = pick(ctx.rng, NOUNS); + const p = pick( + ctx.rng, + PARTICLES.filter((x) => x.level <= ctx.level) + ); + const answer = particle(noun.word, p.kind); + const wrong = p.pair.map((form) => noun.word + form).filter((w) => w !== answer); + if (p.kind === 'direction') wrong.push(noun.word + '으로', noun.word + '로'); + return ask(ctx, { + skill: 'particles', + instruction: `Add the ${p.label} particle`, + prompt: noun.word, + promptLang: 'target', + context: noun.gloss, + answer: [answer], + wrong, + choice: true, + audio: answer, + explain: + p.kind === 'direction' && lastSyllable(noun.word)?.final === 'ㄹ' + ? 'Nouns ending in ㄹ take 로, not 으로.' + : `After a consonant: ${p.pair[0]}; after a vowel: ${p.pair[1]}.` + }); + } + }, + { + id: 'verbs', + title: 'Conjugation', + description: 'Polite, past, future, formal and more, including the ㅂ/ㄷ/르/ㅅ/ㅎ irregulars.', + make: (ctx) => { + const verb = pick( + ctx.rng, + KOREAN_VERBS.filter((v) => v.level <= ctx.level) + ); + const available = (verb.action ? ACTION_FORMS : DESCRIPTIVE_FORMS).filter( + (f) => ctx.level >= 3 || ['present', 'past', 'future', 'negative', 'want'].includes(f) + ); + const form = pick(ctx.rng, available); + const answer = conjugate(verb, form); + return ask(ctx, { + skill: `verbs-${form}`, + instruction: `Conjugate · ${FORM_LABELS[form]}`, + prompt: verb.dict, + promptLang: 'target', + context: verb.gloss, + answer: withAndWithoutSpaces(answer), + wrong: sample(ctx.rng, verbMistakes(verb, form), 6), + audio: answer[0], + explain: verb.irr + ? `${verb.dict} is ${{ b: 'ㅂ', d: 'ㄷ', reu: '르', s: 'ㅅ', h: 'ㅎ' }[verb.irr]}-irregular.` + : undefined + }); + } + }, + { + id: 'numbers', + title: 'Numbers & counters', + description: + 'Two number systems: Sino-Korean for money, dates and minutes; native for counting and hours.', + make: (ctx) => { + const task = countTask(ctx); + return ask(ctx, { + skill: 'numbers', + instruction: 'Say it in Korean', + prompt: task.prompt, + answer: task.answer, + wrong: task.wrong, + audio: task.answer[0], + explain: task.explain + }); + } + } +]; diff --git a/src/lib/language-learner/drills/nl.ts b/src/lib/language-learner/drills/nl.ts new file mode 100644 index 0000000..80b0e97 --- /dev/null +++ b/src/lib/language-learner/drills/nl.ts @@ -0,0 +1,592 @@ +import { dutchNumber } from '../numbers'; +import { int, pick } from '../random'; +import { ask, type DrillMode } from './shared'; + +type Noun = { article: 'de' | 'het'; noun: string; plural: string[]; gloss: string; level: number }; + +const nouns = (level: number, source: string): Noun[] => + source + .trim() + .split('\n') + .map((line) => { + const [word, plural, gloss] = line.split('|').map((p) => p.trim()); + const [article, noun] = word.split(' ') as ['de' | 'het', string]; + return { article, noun, plural: plural ? plural.split(';') : [], gloss, level }; + }); + +export const DUTCH_NOUNS: Noun[] = [ + ...nouns( + 1, + ` +het huis|huizen|house +de hond|honden|dog +de kat|katten|cat +het boek|boeken|book +de tafel|tafels|table +de stoel|stoelen|chair +het raam|ramen|window +de deur|deuren|door +de auto|auto's|car +de fiets|fietsen|bicycle +het kind|kinderen|child +de man|mannen|man +de vrouw|vrouwen|woman +het meisje|meisjes|girl +de jongen|jongens|boy +de vriend|vrienden|friend +het water||water +het brood|broden|bread +de kaas|kazen|cheese +het ei|eieren|egg +de appel|appels|apple +het glas|glazen|glass +de stad|steden|city +het land|landen|country +de straat|straten|street +het station|stations|station +de trein|treinen|train +de dag|dagen|day +de week|weken|week +het jaar|jaren|year +het uur|uren|hour +de school|scholen|school +het bed|bedden|bed +de kamer|kamers|room +de moeder|moeders|mother +de vader|vaders|father +de zus|zussen|sister +de broer|broers|brother +de koffie||coffee +de thee||tea +de melk||milk +het geld||money +de naam|namen|name +de telefoon|telefoons|phone +het oog|ogen|eye +de hand|handen|hand +het weer||weather +de zon||sun +de maan|manen|moon +` + ), + ...nouns( + 2, + ` +de banaan|bananen|banana +het bord|borden|plate +de lepel|lepels|spoon +het mes|messen|knife +de vork|vorken|fork +het dorp|dorpen|village +de weg|wegen|road +de bus|bussen|bus +het vliegtuig|vliegtuigen|airplane +het schip|schepen|ship +de maand|maanden|month +de minuut|minuten|minute +de les|lessen|lesson +de leraar|leraren|teacher +de keuken|keukens|kitchen +de tuin|tuinen|garden +de boom|bomen|tree +de bloem|bloemen|flower +het dier|dieren|animal +het paard|paarden|horse +de koe|koeien|cow +het varken|varkens|pig +de vogel|vogels|bird +de vis|vissen|fish +het hoofd|hoofden|head +het oor|oren|ear +de neus|neuzen|nose +de mond|monden|mouth +de voet|voeten|foot +het been|benen|leg +het hart|harten|heart +de dokter|dokters|doctor +het ziekenhuis|ziekenhuizen|hospital +de winkel|winkels|shop +de prijs|prijzen|price +het bericht|berichten|message +de brief|brieven|letter +het woord|woorden|word +de taal|talen|language +de vraag|vragen|question +het antwoord|antwoorden|answer +de film|films|film +het lied|liederen|song +het spel|spellen|game +het feest|feesten|party +de verjaardag|verjaardagen|birthday +het cadeau|cadeaus|present, gift +de kleur|kleuren|colour +de zee|zeeën|sea +het strand|stranden|beach +de berg|bergen|mountain +de rivier|rivieren|river +het bos|bossen|forest +het eiland|eilanden|island +het gebouw|gebouwen|building +de kerk|kerken|church +het restaurant|restaurants|restaurant +de rekening|rekeningen|bill +het menu|menu's|menu +het bier|bieren|beer +de wijn|wijnen|wine +het vlees||meat +de groente|groenten;groentes|vegetable +het fruit||fruit +de soep|soepen|soup +de broek|broeken|trousers +het overhemd|overhemden|shirt +de jas|jassen|coat +de schoen|schoenen|shoe +de tas|tassen|bag +de sleutel|sleutels|key +het nummer|nummers|number +het adres|adressen|address +het gezin|gezinnen|family, household +het kaartje|kaartjes|ticket +` + ), + ...nouns( + 3, + ` +de baan|banen|job +het bedrijf|bedrijven|company +de vergadering|vergaderingen|meeting +het kantoor|kantoren|office +de fout|fouten|mistake +het verhaal|verhalen|story +de krant|kranten|newspaper +het nieuws||news +de regering|regeringen|government +het beleid||policy +de ervaring|ervaringen|experience +het onderzoek|onderzoeken|research +de oplossing|oplossingen|solution +het voordeel|voordelen|advantage +het nadeel|nadelen|disadvantage +de gewoonte|gewoonten;gewoontes|habit +het gevoel|gevoelens|feeling +de mening|meningen|opinion +het gesprek|gesprekken|conversation +de toekomst||future +het verleden||past +het lichaam|lichamen|body +de gezondheid||health +het geluk||luck, happiness +het museum|musea;museums|museum +het idee|ideeën|idea +het probleem|problemen|problem +de maatschappij|maatschappijen|society +het onderwerp|onderwerpen|subject, topic +de ontwikkeling|ontwikkelingen|development +het gedrag||behaviour +de invloed|invloeden|influence +het doel|doelen|goal +de uitdaging|uitdagingen|challenge +het verschil|verschillen|difference +de wet|wetten|law +het recht|rechten|right, law +de verkiezing|verkiezingen|election +het milieu||environment +de gemeente|gemeenten;gemeentes|municipality +het gebied|gebieden|area +de afspraak|afspraken|appointment +het gevolg|gevolgen|consequence +de reden|redenen|reason +het resultaat|resultaten|result +` + ) +]; + +type Verb = { + inf: string; + gloss: string; + ik: string; + jij: string[]; + hij: string[]; + past: [string[], string]; + participle?: string; + aux?: 'h' | 'z' | 'hz'; + level: number; +}; + +const verbs = (level: number, source: string): Verb[] => + source + .trim() + .split('\n') + .map((line) => { + const [inf, gloss, ik, pastSg, pastPl, participle, aux, present] = line + .split('|') + .map((p) => p.trim()); + const regular = ik.endsWith('t') ? ik : ik + 't'; + const [jij, hij] = present + ? present.split(',').map((v) => v.split(';')) + : [[regular], [regular]]; + return { + inf, + gloss, + ik, + jij, + hij, + past: [pastSg.split(';'), pastPl], + participle: participle || undefined, + aux: (aux || undefined) as Verb['aux'], + level + }; + }); + +export const DUTCH_VERBS: Verb[] = [ + ...verbs( + 1, + ` +zijn|to be|ben|was|waren|geweest|z|bent,is +hebben|to have|heb|had|hadden|gehad|h|hebt,heeft +gaan|to go|ga|ging|gingen|gegaan|z +komen|to come|kom|kwam|kwamen|gekomen|z +doen|to do|doe|deed|deden|gedaan|h +werken|to work|werk|werkte|werkten|gewerkt|h +wonen|to live (somewhere)|woon|woonde|woonden|gewoond|h +spelen|to play|speel|speelde|speelden|gespeeld|h +maken|to make|maak|maakte|maakten|gemaakt|h +leren|to learn|leer|leerde|leerden|geleerd|h +praten|to talk|praat|praatte|praatten|gepraat|h +koken|to cook|kook|kookte|kookten|gekookt|h +eten|to eat|eet|at|aten|gegeten|h +drinken|to drink|drink|dronk|dronken|gedronken|h +zien|to see|zie|zag|zagen|gezien|h +lezen|to read|lees|las|lazen|gelezen|h +kopen|to buy|koop|kocht|kochten|gekocht|h +kunnen|can, to be able to|kan|kon|konden|||kunt;kan,kan +willen|to want|wil|wilde;wou|wilden;wouden|||wilt;wil,wil +moeten|must, to have to|moet|moest|moesten||| +mogen|may, to be allowed to|mag|mocht|mochten|||mag,mag +` + ), + ...verbs( + 2, + ` +fietsen|to cycle|fiets|fietste|fietsten|gefietst|h +dansen|to dance|dans|danste|dansten|gedanst|h +reizen|to travel|reis|reisde|reisden|gereisd|hz +leven|to live (be alive)|leef|leefde|leefden|geleefd|h +betalen|to pay|betaal|betaalde|betaalden|betaald|h +vertellen|to tell|vertel|vertelde|vertelden|verteld|h +antwoorden|to answer|antwoord|antwoordde|antwoordden|geantwoord|h +luisteren|to listen|luister|luisterde|luisterden|geluisterd|h +wachten|to wait|wacht|wachtte|wachtten|gewacht|h +zetten|to put|zet|zette|zetten|gezet|h +huren|to rent|huur|huurde|huurden|gehuurd|h +bellen|to call (phone)|bel|belde|belden|gebeld|h +stoppen|to stop|stop|stopte|stopten|gestopt|h +schrijven|to write|schrijf|schreef|schreven|geschreven|h +spreken|to speak|spreek|sprak|spraken|gesproken|h +nemen|to take|neem|nam|namen|genomen|h +geven|to give|geef|gaf|gaven|gegeven|h +denken|to think|denk|dacht|dachten|gedacht|h +brengen|to bring|breng|bracht|brachten|gebracht|h +zoeken|to look for|zoek|zocht|zochten|gezocht|h +vinden|to find|vind|vond|vonden|gevonden|h +lopen|to walk|loop|liep|liepen|gelopen|hz +slapen|to sleep|slaap|sliep|sliepen|geslapen|h +staan|to stand|sta|stond|stonden|gestaan|h +zitten|to sit|zit|zat|zaten|gezeten|h +liggen|to lie (down)|lig|lag|lagen|gelegen|h +weten|to know (a fact)|weet|wist|wisten|geweten|h +worden|to become|word|werd|werden|geworden|z +blijven|to stay|blijf|bleef|bleven|gebleven|z +vragen|to ask|vraag|vroeg|vroegen|gevraagd|h +zeggen|to say|zeg|zei|zeiden|gezegd|h +kijken|to look, to watch|kijk|keek|keken|gekeken|h +` + ), + ...verbs( + 3, + ` +beginnen|to begin|begin|begon|begonnen|begonnen|z +vergeten|to forget|vergeet|vergat|vergaten|vergeten|hz +vallen|to fall|val|viel|vielen|gevallen|z +helpen|to help|help|hielp|hielpen|geholpen|h +sterven|to die|sterf|stierf|stierven|gestorven|z +rijden|to drive, to ride|rijd|reed|reden|gereden|hz +vliegen|to fly|vlieg|vloog|vlogen|gevlogen|hz +zwemmen|to swim|zwem|zwom|zwommen|gezwommen|hz +zingen|to sing|zing|zong|zongen|gezongen|h +winnen|to win|win|won|wonnen|gewonnen|h +verliezen|to lose|verlies|verloor|verloren|verloren|h +kiezen|to choose|kies|koos|kozen|gekozen|h +sluiten|to close|sluit|sloot|sloten|gesloten|h +bieden|to offer|bied|bood|boden|geboden|h +dragen|to carry, to wear|draag|droeg|droegen|gedragen|h +slaan|to hit|sla|sloeg|sloegen|geslagen|h +lachen|to laugh|lach|lachte|lachten|gelachen|h +wassen|to wash|was|waste|wasten|gewassen|h +bakken|to bake|bak|bakte|bakten|gebakken|h +vriezen|to freeze|vries|vroor|vroren|gevroren|hz +wegen|to weigh|weeg|woog|wogen|gewogen|h +bederven|to spoil|bederf|bedierf|bedierven|bedorven|hz +` + ) +]; + +const PERSONS = [ + { label: 'ik', slot: 'ik', plural: false }, + { label: 'jij', slot: 'jij', plural: false }, + { label: 'hij', slot: 'hij', plural: false }, + { label: 'zij (she)', slot: 'hij', plural: false }, + { label: 'u', slot: 'jij', plural: false }, + { label: 'wij', slot: 'pl', plural: true }, + { label: 'jullie', slot: 'pl', plural: true }, + { label: 'zij (they)', slot: 'pl', plural: true } +] as const; + +const AUX = { + h: { ik: 'heb', jij: 'hebt', hij: 'heeft', pl: 'hebben' }, + z: { ik: 'ben', jij: 'bent', hij: 'is', pl: 'zijn' } +}; + +type Person = (typeof PERSONS)[number]; + +// "u hebt" and "u heeft", "u kunt" and "u kan" are both correct. +const U_EITHER = ['hebben', 'kunnen']; + +function present(verb: Verb, person: Person): string[] { + if (person.slot === 'ik') return [verb.ik]; + if (person.slot === 'pl') return [verb.inf]; + if (person.label === 'u' && U_EITHER.includes(verb.inf)) + return [...new Set([...verb.jij, ...verb.hij])]; + return person.slot === 'jij' ? verb.jij : verb.hij; +} + +function perfect(verb: Verb, person: Person): string[] { + const auxes = verb.aux === 'hz' ? (['h', 'z'] as const) : [verb.aux as 'h' | 'z']; + return auxes + .flatMap((a) => (person.label === 'u' && a === 'h' ? ['hebt', 'heeft'] : [AUX[a][person.slot]])) + .map((aux) => `${aux} ${verb.participle}`); +} + +/** Common learner mistakes, used as wrong options. */ +function mistakes(verb: Verb): string[] { + const stem = verb.ik; + return [ + stem + 'de', + stem + 'te', + stem + 'den', + stem + 'ten', + 'ge' + stem + 'd', + 'ge' + stem + 't', + verb.inf, + stem, + stem + 't', + ...verb.past[0], + verb.past[1] + ]; +} + +const HOURS = [ + 'twaalf', + 'een', + 'twee', + 'drie', + 'vier', + 'vijf', + 'zes', + 'zeven', + 'acht', + 'negen', + 'tien', + 'elf', + 'twaalf' +]; + +/** "half drie" means 2:30: Dutch counts half an hour towards the next hour. */ +export function dutchTime(hour: number, minute: number): string { + const h = HOURS[hour % 12 || 12]; + const next = HOURS[(hour % 12) + 1]; + const phrases: Record = { + 0: `${h} uur`, + 5: `vijf over ${h}`, + 10: `tien over ${h}`, + 15: `kwart over ${h}`, + 20: `tien voor half ${next}`, + 25: `vijf voor half ${next}`, + 30: `half ${next}`, + 35: `vijf over half ${next}`, + 40: `tien over half ${next}`, + 45: `kwart voor ${next}`, + 50: `tien voor ${next}`, + 55: `vijf voor ${next}` + }; + return phrases[minute]; +} + +const numberRange = (level: number) => [20, 100, 1000, 10000, 3_000_000][Math.min(level, 5) - 1]; + +export const drills: DrillMode[] = [ + { + id: 'numbers', + title: 'Numbers', + description: 'Write numbers out in full, from nul to miljoenen.', + make: (ctx) => { + const max = numberRange(ctx.level); + const n = ctx.level >= 4 && ctx.rng() < 0.3 ? int(ctx.rng, 1900, 2099) : int(ctx.rng, 0, max); + const answer = dutchNumber(n); + const near = [ + n + 1, + n - 1, + n + 10, + n - 10, + n + 100, + Number(String(n).split('').reverse().join('')) + ]; + return ask(ctx, { + skill: 'numbers', + instruction: 'Write this number in Dutch', + prompt: n.toLocaleString('nl-NL'), + answer: [answer], + wrong: near.filter((m) => m >= 0 && m !== n).map(dutchNumber), + audio: answer, + explain: + n > 20 && n < 100 ? 'Units come first: vijfentwintig is "five-and-twenty".' : undefined + }); + } + }, + { + id: 'clock', + title: 'Telling time', + description: '"Half drie" is 2:30. Practise the Dutch clock until it is automatic.', + make: (ctx) => { + const hour = int(ctx.rng, 1, 12); + const minute = ctx.level <= 1 ? pick(ctx.rng, [0, 15, 30, 45]) : int(ctx.rng, 0, 11) * 5; + const answer = dutchTime(hour, minute); + const other = (h: number, m: number) => dutchTime(((h + 11) % 12) + 1, m); + return ask(ctx, { + skill: 'clock', + instruction: 'How do you say this time in Dutch?', + prompt: `${hour}:${String(minute).padStart(2, '0')}`, + answer: [answer], + wrong: [ + other(hour - 1, minute), + other(hour + 1, minute), + other(hour, (minute + 30) % 60), + other(hour, (60 - minute) % 60) + ], + audio: answer, + explain: + minute >= 20 && minute <= 40 + ? '20–40 minutes past are counted around "half" the next hour.' + : undefined + }); + } + }, + { + id: 'articles', + title: 'De or het', + description: 'Endless de/het practice. Tip: all plurals and most people are "de".', + make: (ctx) => { + const noun = pick( + ctx.rng, + DUTCH_NOUNS.filter((n) => n.level <= ctx.level) + ); + const answer = noun.article; + return ask(ctx, { + skill: 'articles', + instruction: 'De or het?', + prompt: `___ ${noun.noun}`, + promptLang: 'target', + context: noun.gloss, + answer: [answer], + wrong: [answer === 'de' ? 'het' : 'de'], + choice: true, + audio: `${noun.article} ${noun.noun}`, + explain: noun.noun.endsWith('je') ? 'Diminutives ending in -je are always het.' : undefined + }); + } + }, + { + id: 'plurals', + title: 'Plurals', + description: '-en or -s, and all the spelling changes in between.', + make: (ctx) => { + const noun = pick( + ctx.rng, + DUTCH_NOUNS.filter((n) => n.level <= ctx.level && n.plural.length) + ); + const n = noun.noun; + const doubled = n.replace(/([^aeiou])$/, '$1$1'); + const wrong = [ + n + 'en', + n + 's', + doubled + 'en', + n + 'es', + n.replace(/(aa|ee|oo|uu)([^aeiou])$/, (_, v, c) => v[0] + c) + 'en' + ]; + return ask(ctx, { + skill: 'plurals', + instruction: 'Give the plural', + prompt: `${noun.article} ${n}`, + promptLang: 'target', + context: noun.gloss, + answer: noun.plural.flatMap((p) => [p, `de ${p}`]), + wrong, + audio: `de ${noun.plural[0]}`, + explain: 'All plurals take "de".' + }); + } + }, + { + id: 'verbs', + title: 'Verb forms', + description: 'Present tense, then the perfect (heb gewerkt) and the simple past (werkte).', + make: (ctx) => { + const verb = pick( + ctx.rng, + DUTCH_VERBS.filter((v) => v.level <= ctx.level) + ); + const person = pick(ctx.rng, PERSONS); + const tenses = [ + 'present', + ...(ctx.level >= 2 && verb.participle ? ['perfect'] : []), + ...(ctx.level >= 3 ? ['past'] : []) + ]; + const tense = pick(ctx.rng, tenses); + const answer = + tense === 'present' + ? present(verb, person) + : tense === 'past' + ? person.plural + ? [verb.past[1]] + : verb.past[0] + : perfect(verb, person); + const label = { present: 'present', perfect: 'perfect (heb/ben + ge-)', past: 'simple past' }[ + tense + ]; + const wrong = + tense === 'perfect' + ? [ + `${AUX.h[person.slot]} ge${verb.ik}d`, + `${AUX.z[person.slot]} ${verb.participle}`, + `${AUX.h[person.slot]} ge${verb.ik}t`, + `${AUX.h[person.slot]} ${verb.inf}` + ] + : mistakes(verb); + return ask(ctx, { + skill: `verbs-${tense}`, + instruction: `Conjugate · ${label}`, + prompt: `${person.label} · ${verb.inf}`, + promptLang: 'target', + context: verb.gloss, + answer, + wrong, + explain: + tense === 'past' && !verb.past[0][0].match(/[dt]e$/) + ? `${verb.inf} is a strong verb: its past changes the vowel.` + : tense === 'present' && person.label === 'jij' + ? 'Jij adds -t, except when inverted: "werk jij?".' + : undefined + }); + } + } +]; diff --git a/src/lib/language-learner/drills/shared.ts b/src/lib/language-learner/drills/shared.ts new file mode 100644 index 0000000..bb988f1 --- /dev/null +++ b/src/lib/language-learner/drills/shared.ts @@ -0,0 +1,67 @@ +import { shuffle, type Rng } from '../random'; +import { optionKey } from '../text'; +import type { Course, Exercise } from '../types'; +import type { ExerciseOptions } from '../exercises'; + +export type DrillContext = { rng: Rng; level: number; opts: ExerciseOptions; course: Course }; + +export type DrillMode = { + id: string; + title: string; + description: string; + make: (ctx: DrillContext) => Exercise; +}; + +type Question = { + skill: string; + instruction: string; + prompt: string; + promptLang?: 'target' | 'gloss'; + context?: string; + /** Accepted answers; the first is shown as the correct one. */ + answer: string[]; + wrong: string[]; + explain?: string; + audio?: string; + /** Force multiple choice, e.g. for binary questions like de/het. */ + choice?: boolean; +}; + +/** A typed question when typing is on, otherwise multiple choice with the given wrong answers. */ +export function ask(ctx: DrillContext, q: Question): Exercise { + const key = `${q.skill}:${q.prompt}`; + const base = { + key, + skill: q.skill, + instruction: q.instruction, + prompt: q.prompt, + promptLang: q.promptLang ?? ('gloss' as const), + context: q.context, + entries: [], + explain: q.explain, + audio: q.audio + }; + if (ctx.opts.typing && !q.choice) + return { ...base, type: 'type', accept: q.answer, answerLang: 'target' }; + const taken = new Set(q.answer.map(optionKey)); + const wrong: string[] = []; + for (const w of q.wrong) { + if (taken.has(optionKey(w))) continue; + taken.add(optionKey(w)); + wrong.push(w); + if (wrong.length === 3) break; + } + return { + ...base, + type: 'choose', + options: shuffle(ctx.rng, [q.answer[0], ...wrong]), + answer: q.answer[0], + optionLang: 'target' + }; +} + +/** Accept "세 명" and "세명" alike. */ +export const withAndWithoutSpaces = (answers: string[]) => [ + ...answers, + ...answers.map((a) => a.replace(/ /g, '')).filter((a) => !answers.includes(a)) +]; diff --git a/src/lib/language-learner/exercises.ts b/src/lib/language-learner/exercises.ts new file mode 100644 index 0000000..e98d958 --- /dev/null +++ b/src/lib/language-learner/exercises.ts @@ -0,0 +1,319 @@ +import { isSyllable } from './hangul'; +import { pick, sample, shuffle, type Rng } from './random'; +import { normalize, tiles } from './text'; +import type { + BuildExercise, + ChooseExercise, + Course, + Drill, + Entry, + Exercise, + Lesson, + MatchExercise, + TypeExercise +} from './types'; + +export type ExerciseOptions = { + /** Allow exercises that need a keyboard for the target language. */ + typing: boolean; + /** Speech synthesis is available for the target language. */ + audio: boolean; +}; + +const shown = (values: string[]) => values.join(' / '); + +const overlaps = (a: string[], b: string[]) => { + const set = new Set(a.map(normalize)); + return b.some((value) => set.has(normalize(value))); +}; + +/** Plausible wrong answers: same kind, preferring the same lesson, never sharing an accepted answer. */ +function distractors( + entry: Entry, + pool: Entry[], + side: 'target' | 'gloss', + rng: Rng, + count = 3 +): string[] { + const candidates = pool.filter( + (other) => + other.id !== entry.id && other.kind === entry.kind && !overlaps(other[side], entry[side]) + ); + const near = candidates.filter((other) => other.lesson === entry.lesson); + const values = (list: Entry[]) => list.map((other) => shown(other[side])); + const exclude = [shown(entry[side])]; + const first = sample(rng, values(near), count, String, exclude); + return [ + ...first, + ...sample(rng, values(candidates), count - first.length, String, [...exclude, ...first]) + ]; +} + +export function recognize( + entry: Entry, + pool: Entry[], + rng: Rng, + opts: ExerciseOptions +): ChooseExercise { + const answer = shown(entry.gloss); + const listen = opts.audio && entry.kind !== 'script' && rng() < 0.2; + return { + key: `recognize:${entry.id}`, + type: 'choose', + instruction: listen + ? 'What do you hear?' + : entry.kind === 'script' + ? 'How is this read?' + : 'What does this mean?', + prompt: entry.target[0], + promptLang: 'target', + options: shuffle(rng, [answer, ...distractors(entry, pool, 'gloss', rng)]), + answer, + optionLang: 'gloss', + entries: [entry], + audio: speakable(entry) ? entry.target[0] : undefined, + listen, + explain: entry.note + }; +} + +export function produce(entry: Entry, pool: Entry[], rng: Rng): ChooseExercise { + const answer = shown(entry.target); + return { + key: `produce:${entry.id}`, + type: 'choose', + instruction: entry.kind === 'script' ? 'Which one is it?' : 'How do you say this?', + prompt: shown(entry.gloss), + promptLang: 'gloss', + options: shuffle(rng, [answer, ...distractors(entry, pool, 'target', rng)]), + answer, + optionLang: 'target', + entries: [entry], + audio: speakable(entry) ? entry.target[0] : undefined, + explain: entry.note + }; +} + +export function typeIt(entry: Entry): TypeExercise { + if (entry.kind === 'script') + return { + key: `type:${entry.id}`, + type: 'type', + instruction: 'Type the romanization', + prompt: entry.target[0], + promptLang: 'target', + accept: entry.gloss, + answerLang: 'gloss', + entries: [entry], + explain: entry.note + }; + return { + key: `type:${entry.id}`, + type: 'type', + instruction: entry.kind === 'sentence' ? 'Translate this sentence' : 'Type the translation', + prompt: shown(entry.gloss), + promptLang: 'gloss', + accept: entry.target, + answerLang: 'target', + entries: [entry], + audio: entry.target[0], + explain: entry.note + }; +} + +export function dictation(entry: Entry): TypeExercise { + return { + key: `dictation:${entry.id}`, + type: 'type', + instruction: 'Type what you hear', + prompt: entry.target[0], + promptLang: 'target', + context: shown(entry.gloss), + accept: entry.target, + answerLang: 'target', + entries: [entry], + audio: entry.target[0], + listen: true + }; +} + +export function build(entry: Entry, pool: Entry[], rng: Rng): BuildExercise { + const answer = entry.target[0]; + const own = tiles(answer); + const ownKeys = new Set(own.map(normalize)); + const spare = pool + .filter((other) => other.kind === 'sentence' && other.id !== entry.id) + .flatMap((other) => tiles(other.target[0])) + .filter((tile) => !ownKeys.has(normalize(tile))); + const extra = sample(rng, spare, own.length > 6 ? 2 : 3, normalize); + return { + key: `build:${entry.id}`, + type: 'build', + instruction: 'Build the sentence', + prompt: shown(entry.gloss), + answer, + tiles: shuffle(rng, [...own, ...extra]), + entries: [entry], + audio: answer, + explain: entry.note + }; +} + +/** Blank out one word of a sentence, preferring a word the course teaches. */ +export function cloze(entry: Entry, pool: Entry[], rng: Rng): ChooseExercise | null { + const words = tiles(entry.target[0]); + if (words.length < 3) return null; + const taught = new Set( + pool + .filter((e) => e.kind === 'word') + .flatMap((e) => e.target.flatMap((t) => [normalize(t), normalize(t.split(' ').at(-1)!)])) + ); + const indexes = words.map((_, i) => i).filter((i) => normalize(words[i]).length > 1); + if (!indexes.length) return null; + const preferred = indexes.filter((i) => taught.has(normalize(words[i]))); + const index = pick(rng, preferred.length ? preferred : indexes); + const answer = normalize(words[index]); + const spare = pool + .filter((other) => other.kind === 'sentence' && other.id !== entry.id) + .flatMap((other) => tiles(other.target[0]).map(normalize)) + .filter( + (word) => + word.length > 1 && + !entry.target.some((t) => normalize(t) === normalize(words.with(index, word).join(' '))) + ); + const wrong = sample(rng, spare, 3, String, [answer]); + if (wrong.length < 2) return null; + const trailing = words[index].match(/[.,!?;:…]+$/)?.[0] ?? ''; + return { + key: `cloze:${entry.id}`, + type: 'choose', + instruction: 'Fill in the blank', + prompt: words.with(index, '___' + trailing).join(' '), + promptLang: 'target', + context: shown(entry.gloss), + options: shuffle(rng, [answer, ...wrong]), + answer, + optionLang: 'target', + entries: [entry], + audio: entry.target[0], + explain: entry.note + }; +} + +/** Words to pair up; entries sharing a translation or spelling are left out so every pair is unambiguous. */ +export function match(candidates: Entry[], rng: Rng, count = 5): MatchExercise { + const entries: Entry[] = []; + for (const e of shuffle(rng, candidates)) { + if (entries.length === count) break; + const clash = entries.some( + (x) => + normalize(x.gloss[0]) === normalize(e.gloss[0]) || + normalize(x.target[0]) === normalize(e.target[0]) + ); + if (!clash) entries.push(e); + } + return { + key: `match:${entries.map((e) => e.id).join(',')}`, + type: 'match', + instruction: 'Match the pairs', + pairs: shuffle(rng, entries).map((e) => ({ target: e.target[0], gloss: e.gloss[0] })), + entries + }; +} + +export function fromDrill(drill: Drill, lessonId: string, rng: Rng): ChooseExercise { + return { + key: `drill:${lessonId}:${drill.prompt}`, + type: 'choose', + instruction: 'Choose the right answer', + prompt: drill.prompt, + promptLang: 'target', + options: shuffle(rng, [drill.answer, ...drill.wrong]), + answer: drill.answer, + optionLang: 'target', + entries: [], + explain: drill.explain + }; +} + +/** Script letters on their own are read by their name, not their sound, so skip audio for them. */ +const speakable = (entry: Entry) => + entry.kind !== 'script' || [...entry.target[0]].every(isSyllable); + +/** A lesson run: meet every word, then mixed practice with sentences and grammar questions. */ +export function lessonSession( + course: Course, + lesson: Lesson, + rng: Rng, + opts: ExerciseOptions +): Exercise[] { + const pool = coursePool(course, lesson.level); + const words = lesson.words; + const intro = words.map((w) => recognize(w, pool, rng, { ...opts, audio: false })); + const practice: Exercise[] = []; + for (const w of shuffle(rng, words).slice(0, Math.ceil(words.length / 2))) + practice.push(produce(w, pool, rng)); + if (opts.typing || words.some((w) => w.kind === 'script')) + for (const w of sample(rng, words, 3, (w) => w.id)) + if (opts.typing || w.kind === 'script') practice.push(typeIt(w)); + if (opts.audio) + for (const w of sample(rng, words.filter(speakable), 2, (w) => w.id)) + practice.push({ + ...recognize(w, pool, rng, opts), + listen: true, + instruction: 'What do you hear?', + key: `listen:${w.id}` + }); + if (words.length >= 4) practice.push(match(words, rng)); + const sentences = sample(rng, lesson.sentences, 5, (s) => s.id); + for (const s of sentences) practice.push(build(s, pool, rng)); + for (const s of sample(rng, lesson.sentences, 2, (s) => s.id)) { + const c = cloze(s, pool, rng); + if (c) practice.push(c); + } + if (opts.audio && lesson.sentences.length) + practice.push(dictationOrListen(pick(rng, lesson.sentences), pool, rng, opts)); + const grammar = sample(rng, lesson.drills, 6, (d) => d.prompt).map((d) => + fromDrill(d, lesson.id, rng) + ); + const rest = shuffle(rng, practice).slice(0, Math.max(6, 20 - intro.length - grammar.length)); + return [...intro, ...shuffle(rng, [...rest, ...grammar])]; +} + +function dictationOrListen(entry: Entry, pool: Entry[], rng: Rng, opts: ExerciseOptions): Exercise { + if (opts.typing) return dictation(entry); + return { + ...recognize(entry, pool, rng, opts), + listen: true, + instruction: 'What do you hear?', + key: `listen:${entry.id}` + }; +} + +/** One exercise for an entry, harder the better it is known (0 = new, 1 = well known). */ +export function reviewExercise( + entry: Entry, + pool: Entry[], + rng: Rng, + opts: ExerciseOptions, + strength = 0.5 +): Exercise { + const roll = rng() * 0.6 + strength * 0.4; + if (entry.kind === 'sentence') { + if (roll > 0.75 && opts.typing && tiles(entry.target[0]).length <= 5) return typeIt(entry); + if (roll > 0.55 && opts.audio) return dictationOrListen(entry, pool, rng, opts); + if (roll > 0.35) return cloze(entry, pool, rng) ?? build(entry, pool, rng); + return build(entry, pool, rng); + } + if (roll > 0.6 && (opts.typing || entry.kind === 'script')) return typeIt(entry); + if (roll > 0.4) return produce(entry, pool, rng); + return recognize(entry, pool, rng, opts); +} + +/** Entries a lesson's distractors can come from: its level and the ones around it. */ +export function coursePool(course: Course, levelId: string): Entry[] { + const index = course.levels.findIndex((lv) => lv.id === levelId); + return course.levels + .filter((_, i) => Math.abs(i - index) <= 1) + .flatMap((lv) => lv.lessons.flatMap((ls) => [...ls.words, ...ls.sentences])); +} diff --git a/src/lib/language-learner/hangul.test.ts b/src/lib/language-learner/hangul.test.ts new file mode 100644 index 0000000..5e6a416 --- /dev/null +++ b/src/lib/language-learner/hangul.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from 'vitest'; +import { compose, decompose, particle, romanize } from './hangul'; + +describe('hangul', () => { + it('composes and decomposes syllables', () => { + expect(decompose('한')).toEqual({ initial: 'ㅎ', medial: 'ㅏ', final: 'ㄴ' }); + expect(compose({ initial: 'ㄱ', medial: 'ㅡ', final: 'ㄹ' })).toBe('글'); + }); + + it.each([ + ['안녕하세요', 'annyeonghaseyo'], + ['감사합니다', 'gamsahamnida'], + ['한국어', 'hangugeo'], + ['맛있어요', 'masisseoyo'], + ['읽어요', 'ilgeoyo'], + ['많이', 'mani'], + ['괜찮아요', 'gwaenchanayo'], + ['없어요', 'eopseoyo'], + ['같이', 'gachi'], + ['좋다', 'jota'], + ['신라', 'silla'], + ['설날', 'seollal'], + ['종로', 'jongno'], + ['라면', 'ramyeon'], + ['서울 사람', 'seoul saram'], + ['의사', 'uisa'] + ])('romanizes %s as %s', (hangul, roman) => { + expect(romanize(hangul)).toBe(roman); + }); + + it('chooses particles by the final consonant', () => { + expect(particle('책', 'object')).toBe('책을'); + expect(particle('사과', 'object')).toBe('사과를'); + expect(particle('학생', 'topic')).toBe('학생은'); + expect(particle('저', 'topic')).toBe('저는'); + expect(particle('서울', 'direction')).toBe('서울로'); + expect(particle('집', 'direction')).toBe('집으로'); + expect(particle('의사', 'copula')).toBe('의사예요'); + }); +}); diff --git a/src/lib/language-learner/hangul.ts b/src/lib/language-learner/hangul.ts new file mode 100644 index 0000000..24d7a12 --- /dev/null +++ b/src/lib/language-learner/hangul.ts @@ -0,0 +1,217 @@ +/** Hangul composition, approximate Revised Romanization and particle choice. */ + +export const INITIALS = [...'ㄱㄲㄴㄷㄸㄹㅁㅂㅃㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎ']; +export const MEDIALS = [...'ㅏㅐㅑㅒㅓㅔㅕㅖㅗㅘㅙㅚㅛㅜㅝㅞㅟㅠㅡㅢㅣ']; +export const FINALS = ['', ...'ㄱㄲㄳㄴㄵㄶㄷㄹㄺㄻㄼㄽㄾㄿㅀㅁㅂㅄㅅㅆㅇㅈㅊㅋㅌㅍㅎ']; + +const BASE = 0xac00; + +export type Syllable = { initial: string; medial: string; final: string }; + +export function isSyllable(char: string | undefined): boolean { + if (!char) return false; + const code = char.charCodeAt(0); + return code >= BASE && code <= 0xd7a3; +} + +export function decompose(char: string): Syllable | null { + if (!isSyllable(char)) return null; + const offset = char.charCodeAt(0) - BASE; + return { + initial: INITIALS[Math.floor(offset / 588)], + medial: MEDIALS[Math.floor((offset % 588) / 28)], + final: FINALS[offset % 28] + }; +} + +export function compose({ initial, medial, final }: Syllable): string { + const i = INITIALS.indexOf(initial); + const m = MEDIALS.indexOf(medial); + const f = FINALS.indexOf(final); + if (i < 0 || m < 0 || f < 0) throw new Error(`Cannot compose ${initial}${medial}${final}`); + return String.fromCharCode(BASE + (i * 21 + m) * 28 + f); +} + +/** Replace the final consonant of the last syllable of `word`. */ +export function withFinal(word: string, final: string): string { + const last = decompose(word.at(-1)!); + if (!last) throw new Error(`Not Hangul: ${word}`); + return word.slice(0, -1) + compose({ ...last, final }); +} + +export function lastSyllable(word: string): Syllable | null { + const chars = [...word.replace(/[^가-힣]+$/u, '')]; + return chars.length ? decompose(chars.at(-1)!) : null; +} + +export const hasBatchim = (word: string) => !!lastSyllable(word)?.final; + +const ROMAN_INITIAL: Record = { + ㄱ: 'g', + ㄲ: 'kk', + ㄴ: 'n', + ㄷ: 'd', + ㄸ: 'tt', + ㄹ: 'r', + ㅁ: 'm', + ㅂ: 'b', + ㅃ: 'pp', + ㅅ: 's', + ㅆ: 'ss', + ㅇ: '', + ㅈ: 'j', + ㅉ: 'jj', + ㅊ: 'ch', + ㅋ: 'k', + ㅌ: 't', + ㅍ: 'p', + ㅎ: 'h' +}; +const ROMAN_MEDIAL: Record = { + ㅏ: 'a', + ㅐ: 'ae', + ㅑ: 'ya', + ㅒ: 'yae', + ㅓ: 'eo', + ㅔ: 'e', + ㅕ: 'yeo', + ㅖ: 'ye', + ㅗ: 'o', + ㅘ: 'wa', + ㅙ: 'wae', + ㅚ: 'oe', + ㅛ: 'yo', + ㅜ: 'u', + ㅝ: 'wo', + ㅞ: 'we', + ㅟ: 'wi', + ㅠ: 'yu', + ㅡ: 'eu', + ㅢ: 'ui', + ㅣ: 'i' +}; +/** The sound a final consonant makes before a pause, as one of k/n/t/l/m/p/ng. */ +const FINAL_SOUND: Record = { + ㄱ: 'k', + ㄲ: 'k', + ㄳ: 'k', + ㄺ: 'k', + ㅋ: 'k', + ㄴ: 'n', + ㄵ: 'n', + ㄶ: 'n', + ㄷ: 't', + ㅅ: 't', + ㅆ: 't', + ㅈ: 't', + ㅊ: 't', + ㅌ: 't', + ㅎ: 't', + ㄹ: 'l', + ㄼ: 'l', + ㄽ: 'l', + ㄾ: 'l', + ㅀ: 'l', + ㅁ: 'm', + ㄻ: 'm', + ㅂ: 'p', + ㅄ: 'p', + ㄿ: 'p', + ㅍ: 'p', + ㅇ: 'ng' +}; +/** Compound finals split into the part that stays and the part that moves on. */ +const SPLIT: Record = { + ㄳ: ['ㄱ', 'ㅅ'], + ㄵ: ['ㄴ', 'ㅈ'], + ㄶ: ['', 'ㄴ'], + ㄺ: ['ㄹ', 'ㄱ'], + ㄻ: ['ㄹ', 'ㅁ'], + ㄼ: ['ㄹ', 'ㅂ'], + ㄽ: ['ㄹ', 'ㅅ'], + ㄾ: ['ㄹ', 'ㅌ'], + ㄿ: ['ㄹ', 'ㅍ'], + ㅀ: ['', 'ㄹ'], + ㅄ: ['ㅂ', 'ㅅ'] +}; +const ASPIRATED: Record = { + k: 'ㅋ', + t: 'ㅌ', + p: 'ㅍ', + ㄱ: 'ㅋ', + ㄷ: 'ㅌ', + ㅈ: 'ㅊ' +}; +const NASAL: Record = { k: 'ㅇ', t: 'ㄴ', p: 'ㅁ' }; + +function romanizeWord(chars: Syllable[]): string { + const s = chars.map((c) => ({ ...c })); + for (let j = 0; j < s.length - 1; j++) { + const a = s[j]; + const b = s[j + 1]; + const f = a.final; + if (!f) continue; + const sound = FINAL_SOUND[f]; + if (b.initial === 'ㅇ') { + if (f === 'ㅇ') continue; + if (f === 'ㅎ') a.final = ''; + else if (SPLIT[f]) [a.final, b.initial] = SPLIT[f]; + else [a.final, b.initial] = ['', f]; + // palatalization: 같이 → gachi, 굳이 → guji + if (b.medial === 'ㅣ' && (b.initial === 'ㄷ' || b.initial === 'ㅌ')) + b.initial = b.initial === 'ㄷ' ? 'ㅈ' : 'ㅊ'; + } else if ((f === 'ㅎ' || f === 'ㄶ' || f === 'ㅀ') && ASPIRATED[b.initial]) { + a.final = f === 'ㅎ' ? '' : f === 'ㄶ' ? 'ㄴ' : 'ㄹ'; + b.initial = ASPIRATED[b.initial]; + } else if (b.initial === 'ㅎ' && NASAL[sound]) { + a.final = SPLIT[f]?.[0] ?? ''; + b.initial = ASPIRATED[sound]; + } else if (NASAL[sound] && (b.initial === 'ㄴ' || b.initial === 'ㅁ')) { + a.final = NASAL[sound]; + } else if (NASAL[sound] && b.initial === 'ㄹ') { + a.final = NASAL[sound]; + b.initial = 'ㄴ'; + } else if (sound === 'n' && b.initial === 'ㄹ') { + a.final = 'ㄹ'; + } else if (sound === 'l' && b.initial === 'ㄴ') { + b.initial = 'ㄹ'; + } else if ((sound === 'm' || sound === 'ng') && b.initial === 'ㄹ') { + b.initial = 'ㄴ'; + } + } + return s + .map((c, j) => { + const initial = + c.initial === 'ㄹ' && j > 0 && FINAL_SOUND[s[j - 1].final] === 'l' + ? 'l' + : ROMAN_INITIAL[c.initial]; + return initial + ROMAN_MEDIAL[c.medial] + (c.final ? FINAL_SOUND[c.final] : ''); + }) + .join(''); +} + +/** Approximate Revised Romanization, including the common sound changes between syllables. */ +export function romanize(text: string): string { + return text.replace(/[가-힣]+/gu, (word) => + romanizeWord([...word].map((char) => decompose(char)!)) + ); +} + +export type Particle = 'topic' | 'subject' | 'object' | 'and' | 'with' | 'direction' | 'copula'; + +/** Attach a particle, choosing the form by whether the noun ends in a consonant. */ +export function particle(noun: string, kind: Particle): string { + const last = lastSyllable(noun); + const batchim = !!last?.final; + const forms: Record = { + topic: ['은', '는'], + subject: ['이', '가'], + object: ['을', '를'], + and: ['과', '와'], + with: ['이랑', '랑'], + direction: ['으로', '로'], + copula: ['이에요', '예요'] + }; + if (kind === 'direction' && last?.final === 'ㄹ') return noun + '로'; + return noun + forms[kind][batchim ? 0 : 1]; +} diff --git a/src/lib/language-learner/korean-verbs.test.ts b/src/lib/language-learner/korean-verbs.test.ts new file mode 100644 index 0000000..e61bc32 --- /dev/null +++ b/src/lib/language-learner/korean-verbs.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, it } from 'vitest'; +import { conjugate, KOREAN_VERBS, type KoreanForm } from './korean-verbs'; + +const verb = (dict: string) => { + const found = KOREAN_VERBS.find((v) => v.dict === dict); + if (!found) throw new Error(dict); + return found; +}; +const form = (dict: string, f: KoreanForm) => conjugate(verb(dict), f)[0]; + +describe('korean conjugation', () => { + it.each([ + ['가다', '가요', '갔어요'], + ['오다', '와요', '왔어요'], + ['먹다', '먹어요', '먹었어요'], + ['마시다', '마셔요', '마셨어요'], + ['보다', '봐요', '봤어요'], + ['하다', '해요', '했어요'], + ['공부하다', '공부해요', '공부했어요'], + ['배우다', '배워요', '배웠어요'], + ['쓰다', '써요', '썼어요'], + ['바쁘다', '바빠요', '바빴어요'], + ['크다', '커요', '컸어요'], + ['되다', '돼요', '됐어요'], + ['보내다', '보내요', '보냈어요'], + ['쉬다', '쉬어요', '쉬었어요'], + ['듣다', '들어요', '들었어요'], + ['돕다', '도와요', '도왔어요'], + ['춥다', '추워요', '추웠어요'], + ['모르다', '몰라요', '몰랐어요'], + ['부르다', '불러요', '불렀어요'], + ['낫다', '나아요', '나았어요'], + ['짓다', '지어요', '지었어요'], + ['그렇다', '그래요', '그랬어요'], + ['하얗다', '하얘요', '하얬어요'], + ['좋다', '좋아요', '좋았어요'], + ['받다', '받아요', '받았어요'], + ['웃다', '웃어요', '웃었어요'], + ['살다', '살아요', '살았어요'], + ['기다리다', '기다려요', '기다렸어요'], + ['춤추다', '춤춰요', '춤췄어요'], + ['빠르다', '빨라요', '빨랐어요'] + ])('%s → %s / %s', (dict, present, past) => { + expect(form(dict, 'present')).toBe(present); + expect(form(dict, 'past')).toBe(past); + }); + + it('builds future, formal, request and negative forms', () => { + expect(form('가다', 'future')).toBe('갈 거예요'); + expect(form('먹다', 'future')).toBe('먹을 거예요'); + expect(form('살다', 'future')).toBe('살 거예요'); + expect(form('춥다', 'future')).toBe('추울 거예요'); + expect(form('듣다', 'future')).toBe('들을 거예요'); + expect(form('그렇다', 'future')).toBe('그럴 거예요'); + expect(form('가다', 'formal')).toBe('갑니다'); + expect(form('먹다', 'formal')).toBe('먹습니다'); + expect(form('살다', 'formal')).toBe('삽니다'); + expect(form('만들다', 'formal')).toBe('만듭니다'); + expect(form('읽다', 'request')).toBe('읽으세요'); + expect(form('앉다', 'request')).toBe('앉으세요'); + expect(form('만들다', 'request')).toBe('만드세요'); + expect(form('돕다', 'request')).toBe('도우세요'); + expect(form('듣다', 'request')).toBe('들으세요'); + expect(form('먹다', 'request')).toBe('드세요'); + expect(form('공부하다', 'negative')).toBe('공부 안 해요'); + expect(conjugate(verb('가다'), 'negative')).toEqual(['안 가요', '가지 않아요']); + expect(form('있다', 'negative')).toBe('없어요'); + expect(form('듣다', 'can')).toBe('들을 수 있어요'); + expect(form('보다', 'want')).toBe('보고 싶어요'); + }); + + it('conjugates every listed verb without throwing', () => { + for (const v of KOREAN_VERBS) + for (const f of ['present', 'past', 'future', 'formal', 'request', 'negative'] as const) + expect(conjugate(v, f)[0]).toMatch(/[가-힣]/); + }); +}); diff --git a/src/lib/language-learner/korean-verbs.ts b/src/lib/language-learner/korean-verbs.ts new file mode 100644 index 0000000..7b09416 --- /dev/null +++ b/src/lib/language-learner/korean-verbs.ts @@ -0,0 +1,343 @@ +import { compose, decompose, lastSyllable, withFinal } from './hangul'; + +/** Irregular classes: ㅂ, ㄷ, 르, ㅅ and ㅎ. ㄹ-dropping and 으-dropping are handled as regular rules. */ +export type Irregular = 'b' | 'd' | 'reu' | 's' | 'h'; + +export type KoreanVerb = { + dict: string; + gloss: string; + /** Action verbs take -고 싶어요, -고 있어요 and -(으)세요; descriptive verbs do not. */ + action: boolean; + irr?: Irregular; + /** Noun + 하다 verbs, which negate as 공부 안 해요 rather than 안 공부해요. */ + noun?: boolean; + level: number; +}; + +const replaceLast = (word: string, next: string) => word.slice(0, -1) + next; + +const bright = (medial: string) => medial === 'ㅏ' || medial === 'ㅗ' || medial === 'ㅑ'; + +/** The 아/어 form (casual present), the base for polite present and past. */ +export function infinitive(verb: Pick): string { + const stem = verb.dict.slice(0, -1); + if (stem.endsWith('하')) return replaceLast(stem, '해'); + const last = decompose(stem.at(-1)!)!; + const previous = stem.length > 1 ? decompose(stem.at(-2)!) : null; + + if (verb.irr === 'b' && last.final === 'ㅂ') { + const base = replaceLast(stem, compose({ ...last, final: '' })); + return base + (stem === '돕' || stem === '곱' ? '와' : '워'); + } + if (verb.irr === 'd' && last.final === 'ㄷ') + return withFinal(stem, 'ㄹ') + (bright(last.medial) ? '아' : '어'); + if (verb.irr === 's' && last.final === 'ㅅ') + return withFinal(stem, '') + (bright(last.medial) ? '아' : '어'); + if (verb.irr === 'reu' && stem.endsWith('르') && previous) { + const head = stem.slice(0, -2) + compose({ ...previous, final: 'ㄹ' }); + return head + (bright(previous.medial) ? '라' : '러'); + } + if (verb.irr === 'h' && last.final === 'ㅎ') + return replaceLast( + stem, + compose({ ...last, medial: last.medial === 'ㅑ' ? 'ㅒ' : 'ㅐ', final: '' }) + ); + + if (last.final) return stem + (bright(last.medial) ? '아' : '어'); + + const contracted: Record = { ㅗ: 'ㅘ', ㅜ: 'ㅝ', ㅣ: 'ㅕ', ㅚ: 'ㅙ' }; + switch (last.medial) { + case 'ㅏ': + case 'ㅓ': + case 'ㅐ': + case 'ㅔ': + case 'ㅕ': + return stem; + case 'ㅡ': { + const vowel = previous && bright(previous.medial) ? 'ㅏ' : 'ㅓ'; + return replaceLast(stem, compose({ ...last, medial: vowel })); + } + default: + if (contracted[last.medial]) + return replaceLast(stem, compose({ ...last, medial: contracted[last.medial] })); + return stem + '어'; + } +} + +/** The stem with (으)ㄹ attached, used for the future and for "can". */ +export function futureStem(verb: Pick): string { + const stem = verb.dict.slice(0, -1); + const last = lastSyllable(stem)!; + if (last.final === 'ㄹ') return stem; + if (verb.irr === 'b' && last.final === 'ㅂ') return withFinal(stem, '') + '울'; + if (verb.irr === 'd' && last.final === 'ㄷ') return withFinal(stem, 'ㄹ') + '을'; + if (verb.irr === 's' && last.final === 'ㅅ') return withFinal(stem, '') + '을'; + if (verb.irr === 'h' && last.final === 'ㅎ') return withFinal(stem, 'ㄹ'); + if (!last.final) return withFinal(stem, 'ㄹ'); + return stem + '을'; +} + +const HONORIFIC: Record = { + 먹다: '드세요', + 마시다: '드세요', + 자다: '주무세요', + 있다: '계세요', + 말하다: '말씀하세요' +}; + +export type KoreanForm = + | 'present' + | 'past' + | 'future' + | 'formal' + | 'want' + | 'progressive' + | 'negative' + | 'can' + | 'request' + | 'casual'; + +export const FORM_LABELS: Record = { + present: 'polite present (-아요/어요)', + past: 'polite past (-았어요/었어요)', + future: 'future (-(으)ㄹ 거예요)', + formal: 'formal (-(스)ㅂ니다)', + want: 'want to (-고 싶어요)', + progressive: 'is doing (-고 있어요)', + negative: 'negative (안 / -지 않아요)', + can: 'can (-(으)ㄹ 수 있어요)', + request: 'polite request (-(으)세요)', + casual: 'casual present (반말)' +}; + +export const ACTION_FORMS: KoreanForm[] = Object.keys(FORM_LABELS) as KoreanForm[]; +export const DESCRIPTIVE_FORMS: KoreanForm[] = [ + 'present', + 'past', + 'future', + 'formal', + 'negative', + 'casual' +]; + +/** Every accepted answer for a form; the first one is the canonical answer. */ +export function conjugate(verb: KoreanVerb, form: KoreanForm): string[] { + const stem = verb.dict.slice(0, -1); + const inf = infinitive(verb); + const present = inf + '요'; + switch (form) { + case 'present': + return [present]; + case 'casual': + return [inf]; + case 'past': + return [withFinal(inf, 'ㅆ') + '어요']; + case 'future': + return [futureStem(verb) + ' 거예요']; + case 'can': + return [futureStem(verb) + ' 수 있어요']; + case 'formal': { + const last = lastSyllable(stem)!; + if (last.final === 'ㄹ') return [withFinal(stem, 'ㅂ') + '니다']; + return [last.final ? stem + '습니다' : withFinal(stem, 'ㅂ') + '니다']; + } + case 'want': + return [stem + '고 싶어요']; + case 'progressive': + return [stem + '고 있어요']; + case 'request': { + if (HONORIFIC[verb.dict]) return [HONORIFIC[verb.dict]]; + const last = lastSyllable(stem)!; + if (last.final === 'ㄹ') return [withFinal(stem, '') + '세요']; + if (verb.irr === 'b' && last.final === 'ㅂ') return [withFinal(stem, '') + '우세요']; + if (verb.irr === 'h' && last.final === 'ㅎ') return [withFinal(stem, '') + '세요']; + if (verb.irr === 'd' || verb.irr === 's') return [futureStem(verb).slice(0, -1) + '으세요']; + return [last.final ? stem + '으세요' : stem + '세요']; + } + case 'negative': { + const long = stem + '지 않아요'; + if (verb.dict === '있다') return ['없어요', long]; + if (verb.dict === '알다') return ['몰라요', long]; + if (verb.dict === '맛있다') return ['맛없어요', long]; + if (verb.dict === '재미있다') return ['재미없어요', long]; + if (verb.noun) return [`${verb.dict.slice(0, -2)} 안 해요`, long]; + return [`안 ${present}`, long]; + } + } +} + +const verbs = (level: number, action: boolean, source: string): KoreanVerb[] => + source + .trim() + .split('\n') + .map((line) => { + const [dict, gloss, flags = ''] = line.split('|').map((part) => part.trim()); + const irr = (['b', 'd', 'reu', 's', 'h'] as const).find((i) => flags.split(' ').includes(i)); + // 'plain' marks action verbs that make no sense as a wish or a request (끝나다, 되다) + return { + dict, + gloss, + action: action && !flags.includes('plain'), + level, + irr, + noun: flags.includes('noun') + }; + }); + +export const KOREAN_VERBS: KoreanVerb[] = [ + ...verbs( + 1, + true, + ` +가다|to go +오다|to come +먹다|to eat +마시다|to drink +보다|to see, to watch +하다|to do +자다|to sleep +읽다|to read +사다|to buy +만나다|to meet +공부하다|to study|noun +일하다|to work|noun +좋아하다|to like +앉다|to sit +배우다|to learn +가르치다|to teach +기다리다|to wait +주다|to give +쓰다|to write, to use +살다|to live +알다|to know +만들다|to make +타다|to ride, to take (transport) +입다|to wear (clothes) +받다|to receive +닫다|to close +열다|to open +놀다|to play, to hang out +있다|to be (somewhere), to have +` + ), + ...verbs( + 1, + false, + ` +좋다|to be good +크다|to be big +작다|to be small +많다|to be many, a lot +비싸다|to be expensive +싸다|to be cheap +맛있다|to be tasty +재미있다|to be fun +바쁘다|to be busy +예쁘다|to be pretty +괜찮다|to be okay +` + ), + ...verbs( + 2, + true, + ` +듣다|to listen|d +걷다|to walk|d +돕다|to help|b +부르다|to sing, to call|reu +모르다|to not know|reu +씻다|to wash +팔다|to sell +울다|to cry +웃다|to laugh, to smile +보내다|to send +끝나다|to end|plain +시작하다|to start +운동하다|to exercise|noun +요리하다|to cook|noun +전화하다|to phone|noun +청소하다|to clean|noun +쉬다|to rest +찾다|to look for, to find +빌리다|to borrow +춤추다|to dance +여행하다|to travel|noun +되다|to become|plain +찍다|to take (a photo) +` + ), + ...verbs( + 2, + false, + ` +덥다|to be hot (weather)|b +춥다|to be cold (weather)|b +어렵다|to be difficult|b +쉽다|to be easy|b +맵다|to be spicy|b +가깝다|to be close|b +멀다|to be far +길다|to be long +빠르다|to be fast|reu +다르다|to be different|reu +아프다|to hurt, to be sick +슬프다|to be sad +기쁘다|to be glad +피곤하다|to be tired +조용하다|to be quiet +따뜻하다|to be warm +높다|to be high +나쁘다|to be bad +` + ), + ...verbs( + 3, + true, + ` +묻다|to ask|d +짓다|to build, to make up (a name)|s +붓다|to pour|s +긋다|to draw (a line)|s +자르다|to cut|reu +고르다|to choose|reu +서두르다|to hurry|reu +누르다|to press|reu +굽다|to grill, to bake|b +눕다|to lie down|b +깨닫다|to realize|d +싣다|to load|d +생각하다|to think +이야기하다|to talk|noun +준비하다|to prepare|noun +결정하다|to decide +설명하다|to explain +잊어버리다|to forget +싸우다|to fight +` + ), + ...verbs( + 3, + false, + ` +그렇다|to be like that|h +이렇다|to be like this|h +어떻다|to be how|h +빨갛다|to be red|h +파랗다|to be blue|h +노랗다|to be yellow|h +하얗다|to be white|h +까맣다|to be black|h +귀엽다|to be cute|b +무겁다|to be heavy|b +가볍다|to be light|b +즐겁다|to be enjoyable|b +부럽다|to be envious|b +낫다|to be better|s +넓다|to be wide +좁다|to be narrow +행복하다|to be happy +시원하다|to be cool, refreshing +게으르다|to be lazy|reu +` + ) +]; diff --git a/src/lib/language-learner/modes.ts b/src/lib/language-learner/modes.ts new file mode 100644 index 0000000..6da8b71 --- /dev/null +++ b/src/lib/language-learner/modes.ts @@ -0,0 +1,41 @@ +export type ModeId = 'daily' | 'sprint' | 'survival' | 'endless' | 'review'; + +export type ModeInfo = { + id: ModeId; + title: string; + description: string; + group: 'challenge' | 'review'; +}; + +export const MODES: ModeInfo[] = [ + { + id: 'daily', + title: 'Daily challenge', + description: '20 questions, the same all day. Your first score counts.', + group: 'challenge' + }, + { + id: 'sprint', + title: 'Sprint', + description: 'As many right answers as you can in 60 seconds.', + group: 'challenge' + }, + { + id: 'survival', + title: 'Survival', + description: 'Endless and slowly harder. Three mistakes and you are out.', + group: 'challenge' + }, + { + id: 'endless', + title: 'Mixed practice', + description: 'No timer, no lives. Words, grammar and drills until you stop.', + group: 'challenge' + }, + { + id: 'review', + title: 'Review', + description: 'Spaced repetition: due words first, then your weakest ones.', + group: 'review' + } +]; diff --git a/src/lib/language-learner/numbers.test.ts b/src/lib/language-learner/numbers.test.ts new file mode 100644 index 0000000..aef95f6 --- /dev/null +++ b/src/lib/language-learner/numbers.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from 'vitest'; +import { dutchNumber, englishNumber, nativeKorean, sinoKorean } from './numbers'; + +describe('number words', () => { + it('writes Dutch numbers', () => { + expect(dutchNumber(0)).toBe('nul'); + expect(dutchNumber(21)).toBe('eenentwintig'); + expect(dutchNumber(22)).toBe('tweeëntwintig'); + expect(dutchNumber(83)).toBe('drieëntachtig'); + expect(dutchNumber(100)).toBe('honderd'); + expect(dutchNumber(345)).toBe('driehonderdvijfenveertig'); + expect(dutchNumber(1998)).toBe('duizendnegenhonderdachtennegentig'); + expect(dutchNumber(2026)).toBe('tweeduizendzesentwintig'); + expect(dutchNumber(3_500_000)).toBe('drie miljoen vijfhonderdduizend'); + }); + + it('writes English numbers', () => { + expect(englishNumber(42)).toBe('forty-two'); + expect(englishNumber(105)).toBe('one hundred and five'); + expect(englishNumber(2005)).toBe('two thousand and five'); + expect(englishNumber(1250)).toBe('one thousand two hundred and fifty'); + }); + + it('writes Korean numbers', () => { + expect(sinoKorean(10)).toBe('십'); + expect(sinoKorean(16)).toBe('십육'); + expect(sinoKorean(250)).toBe('이백오십'); + expect(sinoKorean(10000)).toBe('만'); + expect(sinoKorean(35000)).toBe('삼만오천'); + expect(sinoKorean(2026)).toBe('이천이십육'); + expect(nativeKorean(1)).toBe('하나'); + expect(nativeKorean(24)).toBe('스물넷'); + expect(nativeKorean(20, true)).toBe('스무'); + expect(nativeKorean(23, true)).toBe('스물세'); + expect(nativeKorean(4, true)).toBe('네'); + }); +}); diff --git a/src/lib/language-learner/numbers.ts b/src/lib/language-learner/numbers.ts new file mode 100644 index 0000000..713e438 --- /dev/null +++ b/src/lib/language-learner/numbers.ts @@ -0,0 +1,166 @@ +/** Number words, so number drills can generate endless questions. */ + +const NL_UNITS = [ + 'nul', + 'een', + 'twee', + 'drie', + 'vier', + 'vijf', + 'zes', + 'zeven', + 'acht', + 'negen', + 'tien', + 'elf', + 'twaalf', + 'dertien', + 'veertien', + 'vijftien', + 'zestien', + 'zeventien', + 'achttien', + 'negentien' +]; +const NL_TENS = [ + '', + '', + 'twintig', + 'dertig', + 'veertig', + 'vijftig', + 'zestig', + 'zeventig', + 'tachtig', + 'negentig' +]; + +function dutchBelowThousand(n: number): string { + if (n < 20) return NL_UNITS[n]; + if (n < 100) { + const unit = n % 10; + const tens = NL_TENS[Math.floor(n / 10)]; + if (!unit) return tens; + const word = NL_UNITS[unit]; + // tweeëntwintig, drieëntwintig: a diaeresis separates the e's + return word + (word.endsWith('e') ? 'ën' : 'en') + tens; + } + const hundreds = Math.floor(n / 100); + const rest = n % 100; + return ( + (hundreds === 1 ? '' : NL_UNITS[hundreds]) + 'honderd' + (rest ? dutchBelowThousand(rest) : '') + ); +} + +export function dutchNumber(n: number): string { + if (n < 1000) return dutchBelowThousand(n); + if (n < 1_000_000) { + const thousands = Math.floor(n / 1000); + const rest = n % 1000; + return ( + (thousands === 1 ? '' : dutchBelowThousand(thousands)) + + 'duizend' + + (rest ? dutchBelowThousand(rest) : '') + ); + } + const millions = Math.floor(n / 1_000_000); + const rest = n % 1_000_000; + return `${dutchBelowThousand(millions)} miljoen${rest ? ' ' + dutchNumber(rest) : ''}`; +} + +const EN_UNITS = [ + 'zero', + 'one', + 'two', + 'three', + 'four', + 'five', + 'six', + 'seven', + 'eight', + 'nine', + 'ten', + 'eleven', + 'twelve', + 'thirteen', + 'fourteen', + 'fifteen', + 'sixteen', + 'seventeen', + 'eighteen', + 'nineteen' +]; +const EN_TENS = [ + '', + '', + 'twenty', + 'thirty', + 'forty', + 'fifty', + 'sixty', + 'seventy', + 'eighty', + 'ninety' +]; + +function englishBelowThousand(n: number): string { + if (n < 20) return EN_UNITS[n]; + if (n < 100) return EN_TENS[Math.floor(n / 10)] + (n % 10 ? '-' + EN_UNITS[n % 10] : ''); + const rest = n % 100; + return `${EN_UNITS[Math.floor(n / 100)]} hundred${rest ? ' and ' + englishBelowThousand(rest) : ''}`; +} + +/** British style ("one hundred and five"); answers are also accepted without "and". */ +export function englishNumber(n: number): string { + if (n < 1000) return englishBelowThousand(n); + if (n < 1_000_000) { + const rest = n % 1000; + const joiner = rest && rest < 100 ? ' and ' : ' '; + return `${englishBelowThousand(Math.floor(n / 1000))} thousand${rest ? joiner + englishBelowThousand(rest) : ''}`; + } + const rest = n % 1_000_000; + return `${englishBelowThousand(Math.floor(n / 1_000_000))} million${rest ? ' ' + englishNumber(rest) : ''}`; +} + +const SINO = ['', '일', '이', '삼', '사', '오', '육', '칠', '팔', '구']; + +function sinoBelowTenThousand(n: number): string { + const places: [number, string][] = [ + [1000, '천'], + [100, '백'], + [10, '십'] + ]; + let out = ''; + for (const [value, name] of places) { + const digit = Math.floor(n / value) % 10; + if (digit) out += (digit === 1 ? '' : SINO[digit]) + name; + } + return out + SINO[n % 10]; +} + +/** Sino-Korean numbers: dates, money, minutes, phone numbers. 10,000 is 만. */ +export function sinoKorean(n: number): string { + if (n === 0) return '영'; + const man = Math.floor(n / 10000); + const rest = n % 10000; + return ( + (man ? (man === 1 ? '' : sinoBelowTenThousand(man)) + '만' : '') + + (rest ? sinoBelowTenThousand(rest) : '') + ); +} + +const NATIVE_UNITS = ['', '하나', '둘', '셋', '넷', '다섯', '여섯', '일곱', '여덟', '아홉']; +const NATIVE_TENS = ['', '열', '스물', '서른', '마흔', '쉰', '예순', '일흔', '여든', '아흔']; +const COUNTER_UNITS: Record = { 1: '한', 2: '두', 3: '세', 4: '네' }; + +/** Native Korean numbers (1–99): counting things, age, hours. `counter` uses 한/두/세/네/스무. */ +export function nativeKorean(n: number, counter = false): string { + if (n < 1 || n > 99) throw new RangeError('Native Korean numbers run from 1 to 99'); + const tens = Math.floor(n / 10); + const unit = n % 10; + if (counter) { + if (n === 20) return '스무'; + return NATIVE_TENS[tens] + (COUNTER_UNITS[unit] ?? NATIVE_UNITS[unit]); + } + return NATIVE_TENS[tens] + NATIVE_UNITS[unit]; +} diff --git a/src/lib/language-learner/progress.svelte.ts b/src/lib/language-learner/progress.svelte.ts new file mode 100644 index 0000000..2655784 --- /dev/null +++ b/src/lib/language-learner/progress.svelte.ts @@ -0,0 +1,194 @@ +import { browser } from '$app/environment'; +import { dayKey, dayNumber, newCard, review, type Card } from './srs'; +import type { Entry, LangCode } from './types'; + +export type LessonRecord = { best: number; runs: number; last: string }; + +export type LangProgress = { + lessons: Record; + cards: Record; + xp: number; + /** XP earned per local day, `YYYY-MM-DD`. */ + days: Record; + /** First daily-challenge score per day, as a percentage. */ + daily: Record; + /** Best score per challenge mode. */ + bests: Record; + /** Accuracy per endless drill. */ + skills: Record; + /** Level last chosen for mixed challenges; empty means "what I have learned". */ + scope: string; +}; + +export type Settings = { + typing: Partial>; + audio: boolean; + romanization: boolean; + /** Daily XP goal. */ + goal: number; +}; + +type Store = { version: 1; langs: Partial>; settings: Settings }; + +const KEY = 'language-learner-v1'; + +const emptyLang = (): LangProgress => ({ + lessons: {}, + cards: {}, + xp: 0, + days: {}, + daily: {}, + bests: {}, + skills: {}, + scope: '' +}); + +const empty = (): Store => ({ + version: 1, + langs: {}, + settings: { typing: {}, audio: true, romanization: true, goal: 50 } +}); + +function streakOf(days: Record): number { + const today = dayNumber(); + const active = new Set( + Object.entries(days) + .filter(([, xp]) => xp > 0) + .map(([key]) => dayNumber(key)) + ); + // An unfinished today does not break a streak that ran until yesterday. + let day = active.has(today) ? today : today - 1; + let streak = 0; + while (active.has(day)) { + streak++; + day--; + } + return streak; +} + +class Progress { + data = $state(empty()); + loaded = $state(false); + + load() { + if (!browser || this.loaded) return; + try { + const raw = localStorage.getItem(KEY); + if (raw) this.data = { ...empty(), ...JSON.parse(raw) }; + } catch { + // Unreadable progress starts fresh rather than breaking the page. + } + this.loaded = true; + } + + save() { + if (!browser) return; + try { + localStorage.setItem(KEY, JSON.stringify(this.data)); + } catch { + // Storage can be full or disabled; progress then lasts for this visit only. + } + } + + lang(code: LangCode): LangProgress { + return (this.data.langs[code] ??= emptyLang()); + } + + get settings() { + return this.data.settings; + } + + typing(code: LangCode) { + // Typing Hangul needs a Korean keyboard, so it starts off for Korean. + return this.data.settings.typing[code] ?? code !== 'ko'; + } + + answer(code: LangCode, entries: Entry[], correct: boolean) { + const p = this.lang(code); + const today = dayNumber(); + for (const entry of entries) + p.cards[entry.id] = review(p.cards[entry.id] ?? newCard(today), correct, today); + } + + skill(code: LangCode, skill: string, correct: boolean) { + const s = (this.lang(code).skills[skill] ??= { seen: 0, right: 0 }); + s.seen++; + if (correct) s.right++; + } + + addXp(code: LangCode, xp: number) { + const p = this.lang(code); + const key = dayKey(); + p.xp += xp; + p.days[key] = (p.days[key] ?? 0) + xp; + this.save(); + } + + completeLesson(code: LangCode, lessonId: string, score: number, entries: Entry[]) { + const p = this.lang(code); + const record = p.lessons[lessonId]; + p.lessons[lessonId] = { + best: Math.max(record?.best ?? 0, score), + runs: (record?.runs ?? 0) + 1, + last: dayKey() + }; + const today = dayNumber(); + for (const entry of entries) p.cards[entry.id] ??= newCard(today + 1); + this.save(); + } + + recordBest(code: LangCode, mode: string, score: number): boolean { + const p = this.lang(code); + const record = score > (p.bests[mode] ?? 0); + if (record) p.bests[mode] = score; + this.save(); + return record; + } + + recordDaily(code: LangCode, score: number) { + const p = this.lang(code); + p.daily[dayKey()] ??= score; + this.save(); + } + + todayXp(code?: LangCode): number { + const key = dayKey(); + const codes = code ? [code] : (Object.keys(this.data.langs) as LangCode[]); + return codes.reduce((sum, c) => sum + (this.data.langs[c]?.days[key] ?? 0), 0); + } + + streak(code?: LangCode): number { + if (code) return streakOf(this.data.langs[code]?.days ?? {}); + const merged: Record = {}; + for (const p of Object.values(this.data.langs)) + for (const [key, xp] of Object.entries(p.days)) merged[key] = (merged[key] ?? 0) + xp; + return streakOf(merged); + } + + due(code: LangCode): string[] { + const today = dayNumber(); + return Object.entries(this.data.langs[code]?.cards ?? {}) + .filter(([, card]) => card.due <= today) + .sort(([, a], [, b]) => a.due - b.due) + .map(([id]) => id); + } + + export(): string { + return JSON.stringify(this.data); + } + + import(json: string) { + const parsed = JSON.parse(json) as Store; + if (parsed?.version !== 1 || typeof parsed.langs !== 'object') + throw new Error('Not a language learner backup'); + this.data = { ...empty(), ...parsed }; + this.save(); + } + + reset(code: LangCode) { + delete this.data.langs[code]; + this.save(); + } +} + +export const progress = new Progress(); diff --git a/src/lib/language-learner/random.test.ts b/src/lib/language-learner/random.test.ts new file mode 100644 index 0000000..a678003 --- /dev/null +++ b/src/lib/language-learner/random.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from 'vitest'; +import { sample, seeded } from './random'; + +describe('sample', () => { + it('returns at most the requested number of distinct values', () => { + const rng = seeded(1); + expect(sample(rng, [1, 2, 3, 4, 5], 0)).toEqual([]); + expect(sample(rng, [1, 2, 3, 4, 5], 3)).toHaveLength(3); + expect(sample(rng, ['a', 'A', 'b'], 5)).toHaveLength(2); + expect(sample(rng, ['a', 'b', 'c'], 3, String, ['b'])).not.toContain('b'); + }); +}); diff --git a/src/lib/language-learner/random.ts b/src/lib/language-learner/random.ts new file mode 100644 index 0000000..d79dabc --- /dev/null +++ b/src/lib/language-learner/random.ts @@ -0,0 +1,50 @@ +export type Rng = () => number; + +/** Small deterministic generator, so a daily challenge is the same all day. */ +export function seeded(seed: number | string): Rng { + let h = typeof seed === 'number' ? seed >>> 0 : 2166136261; + if (typeof seed === 'string') { + for (let i = 0; i < seed.length; i++) h = Math.imul(h ^ seed.charCodeAt(i), 16777619); + } + return () => { + h = (h + 0x6d2b79f5) >>> 0; + let t = h; + t = Math.imul(t ^ (t >>> 15), t | 1); + t ^= t + Math.imul(t ^ (t >>> 7), t | 61); + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +export const int = (rng: Rng, min: number, max: number) => + min + Math.floor(rng() * (max - min + 1)); + +export const pick = (rng: Rng, list: readonly T[]): T => list[Math.floor(rng() * list.length)]; + +export function shuffle(rng: Rng, list: readonly T[]): T[] { + const copy = [...list]; + for (let i = copy.length - 1; i > 0; i--) { + const j = Math.floor(rng() * (i + 1)); + [copy[i], copy[j]] = [copy[j], copy[i]]; + } + return copy; +} + +/** Up to `count` distinct values from `list` that differ from `exclude` once normalized. */ +export function sample( + rng: Rng, + list: readonly T[], + count: number, + key: (value: T) => string = String, + exclude: string[] = [] +): T[] { + const seen = new Set(exclude.map((value) => value.toLowerCase())); + const out: T[] = []; + for (const value of shuffle(rng, list)) { + if (out.length >= count) break; + const k = key(value).toLowerCase(); + if (seen.has(k)) continue; + seen.add(k); + out.push(value); + } + return out; +} diff --git a/src/lib/language-learner/speech.ts b/src/lib/language-learner/speech.ts new file mode 100644 index 0000000..0b06b2f --- /dev/null +++ b/src/lib/language-learner/speech.ts @@ -0,0 +1,43 @@ +/** Text-to-speech through the browser's built-in voices; no audio files or services. */ + +const supported = () => typeof window !== 'undefined' && 'speechSynthesis' in window; + +function voices(): Promise { + if (!supported()) return Promise.resolve([]); + const now = speechSynthesis.getVoices(); + if (now.length) return Promise.resolve(now); + return new Promise((resolve) => { + const done = () => resolve(speechSynthesis.getVoices()); + speechSynthesis.addEventListener('voiceschanged', done, { once: true }); + setTimeout(done, 1500); + }); +} + +const matches = (voice: SpeechSynthesisVoice, lang: string) => + voice.lang.replace('_', '-').toLowerCase().startsWith(lang.slice(0, 2).toLowerCase()); + +function best(list: SpeechSynthesisVoice[], lang: string) { + const candidates = list.filter((v) => matches(v, lang)); + return ( + candidates.find( + (v) => v.lang.replace('_', '-').toLowerCase() === lang.toLowerCase() && !v.localService + ) ?? + candidates.find((v) => v.lang.replace('_', '-').toLowerCase() === lang.toLowerCase()) ?? + candidates[0] + ); +} + +export async function hasVoice(lang: string): Promise { + return !!best(await voices(), lang); +} + +export async function speak(text: string, lang: string, rate = 0.9) { + if (!supported()) return; + const voice = best(await voices(), lang); + speechSynthesis.cancel(); + const utterance = new SpeechSynthesisUtterance(text); + utterance.lang = lang; + utterance.rate = rate; + if (voice) utterance.voice = voice; + speechSynthesis.speak(utterance); +} diff --git a/src/lib/language-learner/srs.test.ts b/src/lib/language-learner/srs.test.ts new file mode 100644 index 0000000..c44a191 --- /dev/null +++ b/src/lib/language-learner/srs.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from 'vitest'; +import { dayKey, dayNumber, mastery, newCard, review } from './srs'; + +describe('spaced repetition', () => { + it('grows intervals when answers are right and resets them when wrong', () => { + let card = newCard(100); + card = review(card, true, 100); + expect(card.due).toBe(101); + card = review(card, true, 101); + expect(card.due).toBe(104); + card = review(card, true, 104); + expect(card.interval).toBeGreaterThan(3); + card = review(card, false, 110); + expect(card).toMatchObject({ interval: 0, due: 110, lapses: 1 }); + }); + + it('does not reschedule twice on the same day', () => { + let card = review(newCard(5), true, 5); + card = review(card, true, 5); + expect(card).toMatchObject({ reps: 1, due: 6, seen: 2 }); + expect(mastery(card)).toBe('learning'); + }); + + it('counts calendar days', () => { + expect(dayNumber('2026-03-02') - dayNumber('2026-02-27')).toBe(3); + expect(dayKey(new Date(2026, 0, 5))).toBe('2026-01-05'); + }); +}); diff --git a/src/lib/language-learner/srs.ts b/src/lib/language-learner/srs.ts new file mode 100644 index 0000000..ee19013 --- /dev/null +++ b/src/lib/language-learner/srs.ts @@ -0,0 +1,76 @@ +/** A simplified SM-2 schedule. Days are counted as local calendar days. */ + +export type Card = { + due: number; + interval: number; + ease: number; + reps: number; + lapses: number; + seen: number; + right: number; + last: number; +}; + +export function dayKey(date = new Date()): string { + const y = date.getFullYear(); + const m = String(date.getMonth() + 1).padStart(2, '0'); + const d = String(date.getDate()).padStart(2, '0'); + return `${y}-${m}-${d}`; +} + +export function dayNumber(key = dayKey()): number { + const [y, m, d] = key.split('-').map(Number); + return Math.round(Date.UTC(y, m - 1, d) / 86_400_000); +} + +export const newCard = (today: number): Card => ({ + due: today, + interval: 0, + ease: 2.5, + reps: 0, + lapses: 0, + seen: 0, + right: 0, + last: -1 +}); + +export function review(card: Card, correct: boolean, today: number): Card { + const next = { ...card, seen: card.seen + 1, right: card.right + (correct ? 1 : 0) }; + if (!correct) { + return { + ...next, + reps: 0, + interval: 0, + lapses: card.lapses + 1, + ease: Math.max(1.3, card.ease - 0.2), + due: today, + last: today + }; + } + // Grinding the same card again on one day should not push it months ahead. + if (card.last === today && card.reps > 0) return next; + const reps = card.reps + 1; + const interval = + reps === 1 ? 1 : reps === 2 ? 3 : Math.round(Math.max(card.interval, 1) * card.ease); + return { + ...next, + reps, + interval, + ease: Math.min(3, card.ease + 0.05), + due: today + interval, + last: today + }; +} + +export type Mastery = 'new' | 'learning' | 'familiar' | 'known' | 'mastered'; + +export function mastery(card: Card | undefined): Mastery { + if (!card || card.seen === 0) return 'new'; + if (card.interval < 3) return 'learning'; + if (card.interval < 14) return 'familiar'; + if (card.interval < 60) return 'known'; + return 'mastered'; +} + +/** 0 for new cards, approaching 1 for cards reviewed months apart. */ +export const strength = (card: Card | undefined) => (card ? Math.min(1, card.interval / 30) : 0); diff --git a/src/lib/language-learner/text.ts b/src/lib/language-learner/text.ts new file mode 100644 index 0000000..7a2f96a --- /dev/null +++ b/src/lib/language-learner/text.ts @@ -0,0 +1,67 @@ +/** Lowercase, strip punctuation and diacritics-insensitive comparison helpers for answers. */ + +const PUNCTUATION = /[.,!?¿¡;:"“”„‘’'`()[\]{}…~·-]/g; + +export function normalize(value: string): string { + return value + .normalize('NFC') + .toLowerCase() + .replace(/[’‘`]/g, "'") + .replace(PUNCTUATION, ' ') + .replace(/\s+/g, ' ') + .trim(); +} + +export function stripDiacritics(value: string): string { + // Only Latin marks: decomposing Hangul would break Korean answers. + return value.replace(/[À-ɏ]/g, (c) => c.normalize('NFD').replace(/[̀-ͯ]/g, '')); +} + +export function distance(a: string, b: string): number { + const row = Array.from({ length: b.length + 1 }, (_, i) => i); + for (let i = 1; i <= a.length; i++) { + let previous = row[0]; + row[0] = i; + for (let j = 1; j <= b.length; j++) { + const current = row[j]; + row[j] = Math.min(row[j] + 1, row[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1)); + previous = current; + } + } + return row[b.length]; +} + +export type Verdict = 'correct' | 'accent' | 'typo' | 'wrong'; + +/** Compare a typed answer with every accepted answer, forgiving accents and one-letter typos. */ +export function grade(input: string, accepted: string[]): { verdict: Verdict; match: string } { + const given = normalize(input); + let best: { verdict: Verdict; match: string } = { verdict: 'wrong', match: accepted[0] }; + if (!given) return best; + for (const answer of accepted) { + const expected = normalize(answer); + if (given === expected) return { verdict: 'correct', match: answer }; + if (stripDiacritics(given) === stripDiacritics(expected)) + best = { verdict: 'accent', match: answer }; + else if ( + best.verdict === 'wrong' && + [...expected].length >= 5 && + distance(stripDiacritics(given), stripDiacritics(expected)) === 1 + ) + best = { verdict: 'typo', match: answer }; + } + return best; +} + +/** Split a sentence into word tiles, keeping punctuation attached to the word. */ +export function tiles(sentence: string): string[] { + return sentence.split(/\s+/).filter(Boolean); +} + +export function sameTiles(a: string[], b: string[]): boolean { + return normalize(a.join(' ')) === normalize(b.join(' ')); +} + +/** Key for telling multiple-choice options apart; unlike `normalize` it keeps apostrophes (its / it's). */ +export const optionKey = (value: string) => + value.normalize('NFC').trim().toLowerCase().replace(/\s+/g, ' '); diff --git a/src/lib/language-learner/types.ts b/src/lib/language-learner/types.ts new file mode 100644 index 0000000..5b8897b --- /dev/null +++ b/src/lib/language-learner/types.ts @@ -0,0 +1,114 @@ +export type LangCode = 'nl' | 'ko' | 'en'; + +export type EntryKind = 'word' | 'sentence' | 'script'; + +/** Something to learn: a word, a sentence or a Hangul letter. */ +export type Entry = { + /** Stable across content edits, so spaced-repetition history survives reordering. */ + id: string; + kind: EntryKind; + /** Accepted forms in the language being learned; the first is shown. */ + target: string[]; + /** Accepted translations (or romanization, for script entries); the first is shown. */ + gloss: string[]; + note?: string; + lesson: string; +}; + +/** A hand-written multiple-choice question about grammar. */ +export type Drill = { prompt: string; answer: string; wrong: string[]; explain?: string }; + +export type Lesson = { + id: string; + title: string; + summary: string; + /** Markdown. */ + notes: string; + words: Entry[]; + sentences: Entry[]; + drills: Drill[]; + level: string; +}; + +export type Level = { + id: string; + title: string; + /** CEFR band, or "Hangul" for the Korean alphabet. */ + band: string; + description: string; + lessons: Lesson[]; +}; + +export type Course = { + code: LangCode; + name: string; + nativeName: string; + /** Language of translations and explanations. */ + glossLang: LangCode; + /** BCP 47 tag for speech synthesis. */ + voice: string; + tagline: string; + levels: Level[]; +}; + +type Base = { + key: string; + instruction: string; + /** Entries this exercise tests; answers update their review schedule. */ + entries: Entry[]; + explain?: string; + /** Text to read aloud in the target language. */ + audio?: string; + /** Hide the written prompt so the learner has to listen. */ + listen?: boolean; + /** Skill name for endless drills, used in stats. */ + skill?: string; +}; + +export type ChooseExercise = Base & { + type: 'choose'; + prompt: string; + promptLang: 'target' | 'gloss'; + /** Sentence context shown under the prompt, e.g. the translation of a cloze. */ + context?: string; + options: string[]; + answer: string; + optionLang: 'target' | 'gloss'; +}; + +export type TypeExercise = Base & { + type: 'type'; + prompt: string; + promptLang: 'target' | 'gloss'; + context?: string; + accept: string[]; + answerLang: 'target' | 'gloss'; +}; + +export type BuildExercise = Base & { + type: 'build'; + prompt: string; + answer: string; + tiles: string[]; +}; + +export type MatchExercise = Base & { + type: 'match'; + pairs: { target: string; gloss: string }[]; +}; + +export type Exercise = ChooseExercise | TypeExercise | BuildExercise | MatchExercise; + +export type Answer = { correct: boolean; given: string; note?: string }; + +export type SessionResult = { + answered: number; + correct: number; + /** Exercises answered right on the first try. */ + firstTry: number; + total: number; + xp: number; + seconds: number; + mistakes: Exercise[]; + reason: 'done' | 'lives' | 'time' | 'ended'; +}; diff --git a/src/lib/language-learner/ui/Exercise.svelte b/src/lib/language-learner/ui/Exercise.svelte new file mode 100644 index 0000000..c8d0940 --- /dev/null +++ b/src/lib/language-learner/ui/Exercise.svelte @@ -0,0 +1,294 @@ + + + + +
+

{exercise.instruction}

+ + {#if exercise.type === 'choose' || exercise.type === 'type'} +
+ {#if exercise.listen && exercise.audio && audio} + + + {:else} +

+ {exercise.prompt} + {#if exercise.audio && audio && exercise.promptLang === 'target'} + + {/if} +

+ {#if exercise.promptLang === 'target' && roman(exercise.prompt)} +

{roman(exercise.prompt)}

+ {/if} + {/if} + {#if exercise.context} +

{exercise.context}

+ {/if} +
+ {/if} + + {#if exercise.type === 'choose'} +
o.length > 28)}> + {#each exercise.options as option, i (option)} + + {/each} +
+ {:else if exercise.type === 'type'} +
{ + event.preventDefault(); + submitTyped(); + }} + > + { + if (event.key === 'Enter') { + event.preventDefault(); + submitTyped(); + } + }} + /> + +
+ {#if code === 'ko' && exercise.answerLang === 'target'} +

+ Needs a Korean keyboard. Turn typing off in the settings if you don't have one. +

+ {/if} + {:else if exercise.type === 'build'} +
+

{exercise.prompt}

+
+
+ {#each picked as tileIndex, i (tileIndex)} + + {:else} + Tap the words in the right order + {/each} +
+
+ {#each exercise.tiles as tile, i (i)} + + {/each} +
+
+ +
+ {:else if exercise.type === 'match'} +
+
+ {#each exercise.pairs as pair, i (i)} + + {/each} +
+
+ {#each rightOrder as i (i)} + + {/each} +
+
+ {/if} +
diff --git a/src/lib/language-learner/ui/Results.svelte b/src/lib/language-learner/ui/Results.svelte new file mode 100644 index 0000000..6e1fe56 --- /dev/null +++ b/src/lib/language-learner/ui/Results.svelte @@ -0,0 +1,79 @@ + + +
+

{subtitle}

+

{title}{record ? ' · new best!' : ''}

+
+
+

+ {score ?? accuracy}{score === undefined || percent ? '%' : ''} +

+

{score === undefined ? 'accuracy' : 'score'}

+
+
+

{result.correct}/{result.answered}

+

right

+
+
+

+{result.xp}

+

XP

+
+
+ {#if unique.length} +

to look at again

+
    + {#each unique.slice(0, 12) as m (m.key)} +
  • + {m.type === 'match' ? 'Matching pairs' : m.prompt} + {answerOf(m)} +
  • + {/each} +
+ {/if} +
+ {@render children?.()} +
+
diff --git a/src/lib/language-learner/ui/Session.svelte b/src/lib/language-learner/ui/Session.svelte new file mode 100644 index 0000000..4458efd --- /dev/null +++ b/src/lib/language-learner/ui/Session.svelte @@ -0,0 +1,246 @@ + + + + +
+
+ +
+ {#if seconds} + + {:else if !endless} + + {:else} +

{correct} right · {answered - correct} wrong

+ {/if} +
+ {#if seconds} + + {/if} + {#if lives} + + {#each Array.from({ length: lives }, (_, i) => i) as i (i)} + + {/if} + {#if combo >= 3} + {combo} in a row + {/if} + {xp} XP +
+ + {#if current} + {#key turn} + + {/key} + {/if} + + {#if feedback} + + {/if} +

Keys: 1–4 to choose, Enter to check and continue.

+
diff --git a/src/lib/language-learner/ui/Speak.svelte b/src/lib/language-learner/ui/Speak.svelte new file mode 100644 index 0000000..4860dc8 --- /dev/null +++ b/src/lib/language-learner/ui/Speak.svelte @@ -0,0 +1,22 @@ + + + diff --git a/src/lib/projects.ts b/src/lib/projects.ts index 3ccc391..c24308c 100644 --- a/src/lib/projects.ts +++ b/src/lib/projects.ts @@ -35,6 +35,12 @@ export const projects = [ href: '/projects/world-opinions', external: false }, + { + name: 'language learner', + subtitle: 'Dutch, Korean and English, from zero to fluent', + href: '/projects/language-learner', + external: false + }, { name: 'money tracker', subtitle: 'a manual ledger for accounts, bills and utilities', diff --git a/src/lib/seo.ts b/src/lib/seo.ts index 93e0d92..f9f2cf7 100644 --- a/src/lib/seo.ts +++ b/src/lib/seo.ts @@ -9,7 +9,9 @@ type Article = { }; type List = { name: string; isProgress: boolean; items: { name: string }[] }; type Race = { short: string; distance: number; discipline: string }; -type PageContent = { blogArticle?: Article; list?: List; race?: Race }; +/** Pages that describe themselves, such as language learner lessons. */ +type Described = { title: string; description: string; index?: boolean }; +type PageContent = { blogArticle?: Article; list?: List; race?: Race; seo?: Described }; type Seo = { title: string; @@ -49,6 +51,11 @@ const publicPages: Record = { title: 'osu! map finder', description: 'Find osu! beatmaps for Spotify songs, albums and playlists.' }, + '/projects/language-learner': { + title: 'Language Learner', + description: + 'Learn Dutch, Korean and English from beginner to advanced, with lessons and endless daily challenges.' + }, '/projects/world-opinions': { title: 'World Opinions', description: @@ -102,6 +109,8 @@ export function getSeo( ? `${list.name}: ${names.join(', ')}${list.items.length > names.length ? ', and more' : ''}.` : `${list.name}, a ${list.isProgress ? 'progress ' : ''}list by Stan Runge.` }; + } else if (content.seo) { + page = content.seo; } else if (/^\/projects\/derby\/[^/]+$/.test(path) && content.race) { const race = content.race; page = { @@ -114,7 +123,8 @@ export function getSeo( title: page ? formatTitle(page.title, environment) : getTitle(path, environment), description: page?.description ?? 'Stan Runge’s personal website.', url, - indexable: !!page && !environment && !/^\/blog\/[^/]+\/edit$/.test(path), + indexable: + !!page && !environment && !/^\/blog\/[^/]+\/edit$/.test(path) && content.seo?.index !== false, type, image: `${SITE_ORIGIN}/moon.jpg`, imageAlt: 'Moon icon for Stan Runge’s website', diff --git a/src/lib/title.ts b/src/lib/title.ts index ae5ae50..374623a 100644 --- a/src/lib/title.ts +++ b/src/lib/title.ts @@ -29,6 +29,7 @@ export function getTitle(path: string, environment: SiteEnvironment = null): str '/projects/derby': 'Derby Lens', '/projects/cs-prices': 'CS Prices', '/projects/game-tracker': 'Game Tracker', + '/projects/language-learner': 'Language Learner', '/projects/money-tracker': 'Money Tracker', '/projects/spotify-beatmap-finder': 'osu! map finder', '/projects/progress': 'Progress', diff --git a/src/routes/projects/+page.svelte b/src/routes/projects/+page.svelte index b56d3b6..fc31b3e 100644 --- a/src/routes/projects/+page.svelte +++ b/src/routes/projects/+page.svelte @@ -10,6 +10,7 @@ { name: 'cs prices', href: '/projects/cs-prices', external: false }, { name: 'osu! map finder', href: '/projects/spotify-beatmap-finder', external: false }, { name: 'game tracker', href: '/projects/game-tracker', external: false }, + { name: 'language learner', href: '/projects/language-learner', external: false }, { name: 'money tracker', href: '/projects/money-tracker', external: false }, { name: 'world opinions', href: '/projects/world-opinions', external: false }, { name: 'vash esports', href: 'https://esports.vash.software', external: true }, diff --git a/src/routes/projects/language-learner/+layout.svelte b/src/routes/projects/language-learner/+layout.svelte new file mode 100644 index 0000000..6748599 --- /dev/null +++ b/src/routes/projects/language-learner/+layout.svelte @@ -0,0 +1,13 @@ + + +
+ {@render children()} +
diff --git a/src/routes/projects/language-learner/+page.server.ts b/src/routes/projects/language-learner/+page.server.ts new file mode 100644 index 0000000..d91a351 --- /dev/null +++ b/src/routes/projects/language-learner/+page.server.ts @@ -0,0 +1,20 @@ +import { allEntries, allLessons } from '$lib/language-learner/course'; +import { COURSES, loadCourse } from '$lib/language-learner/courses'; +import type { PageServerLoad } from './$types'; + +// Counted on the server so the overview doesn't download three full courses. +export const load: PageServerLoad = async () => { + const courses = await Promise.all(COURSES.map((c) => loadCourse(c.code))); + return { + counts: Object.fromEntries( + courses.map((c) => [ + c.code, + { + lessons: allLessons(c).length, + words: allEntries(c).filter((e) => e.kind === 'word').length, + levels: c.levels.map((lv) => lv.band) + } + ]) + ) as Record + }; +}; diff --git a/src/routes/projects/language-learner/+page.svelte b/src/routes/projects/language-learner/+page.svelte new file mode 100644 index 0000000..63b09ee --- /dev/null +++ b/src/routes/projects/language-learner/+page.svelte @@ -0,0 +1,245 @@ + + +
+

language learner

+

+ Dutch, Korean and English from the very first word to advanced. Structured lessons from A1 to + C1, spaced repetition, and endless daily challenges to grind. +

+
+ +{#if progress.loaded} +
+
+

streak

+

+

+
+
+

today

+

+ {progress.todayXp()} + / {progress.settings.goal} XP +

+ +
+
+

total

+

+ {Object.values(progress.data.langs).reduce((n, p) => n + (p?.xp ?? 0), 0)} XP +

+
+
+{/if} + +
+ {#each COURSES as c (c.code)} + {@const count = data.counts[c.code]} + +
+ {c.code.toUpperCase()} + {#if progress.loaded && progress.streak(c.code)} + + {/if} +
+
+

{c.name}

+

{c.nativeName}

+
+

{c.tagline}

+

+ {count.lessons} lessons · {count.words} words · {count.levels[0]}–{count.levels.at(-1)} +

+
+ {#if progress.loaded} +
+ {completed(c.code)} / {count.lessons} lessons + {#if progress.due(c.code).length} + {progress.due(c.code).length} to review + {/if} +
+ + {/if} + {completed(c.code) ? 'Continue' : 'Start'} + +
+
+ {/each} +
+ +
+
+

how it works

+
    +
  • + Lessons explain one topic, then drill its words, + sentences and grammar. Wrong answers come back until you get them right. +
  • +
  • + Reviews use spaced repetition: every word you + learn returns just before you'd forget it. +
  • +
  • + Challenges are endless: a daily challenge, a + 60-second sprint, survival with three lives, and generated drills for numbers, clock times, conjugation, + articles and particles. +
  • +
  • + Start anywhere. Lessons are never locked, and + a level test lets you skip what you already know. +
  • +
+
+
+

settings

+ {#if progress.loaded} +
+ + + {#each COURSES as c (c.code)} + + {/each} +
+ Daily goal +
+ {#each goals as goal (goal)} + + {/each} +
+
+
+

+ Progress is saved in this browser. Export it to move it to another device. +

+
+ + + + {#each COURSES as c (c.code)} + {#if progress.data.langs[c.code]} + + {/if} + {/each} +
+ {#if importError}

{importError}

{/if} +
+
+ {/if} +
+
diff --git a/src/routes/projects/language-learner/[lang]/+layout.ts b/src/routes/projects/language-learner/[lang]/+layout.ts new file mode 100644 index 0000000..8b353b6 --- /dev/null +++ b/src/routes/projects/language-learner/[lang]/+layout.ts @@ -0,0 +1,10 @@ +import { error } from '@sveltejs/kit'; +import { isLang, loadCourse } from '$lib/language-learner/courses'; +import { loadDrills } from '$lib/language-learner/drills'; +import type { LayoutLoad } from './$types'; + +export const load: LayoutLoad = async ({ params }) => { + if (!isLang(params.lang)) error(404, 'Unknown language'); + const [course, drills] = await Promise.all([loadCourse(params.lang), loadDrills(params.lang)]); + return { course, drills }; +}; diff --git a/src/routes/projects/language-learner/[lang]/+page.svelte b/src/routes/projects/language-learner/[lang]/+page.svelte new file mode 100644 index 0000000..5f430f2 --- /dev/null +++ b/src/routes/projects/language-learner/[lang]/+page.svelte @@ -0,0 +1,211 @@ + + + + +
+
+

{course.name}

+

+ {course.nativeName} · + {course.tagline} +

+
+ {#if progress.loaded} +
+ + {progress.todayXp(code)} XP today + {p?.xp ?? 0} XP total +
+ {/if} +
+ +
+
+ {#if next} +
+

+ {p && Object.keys(p.lessons).length ? 'next lesson' : 'start here'} +

+

{next.title}

+

{next.summary}

+
+ Start lesson + {:else} +
+

course complete

+

Every lesson done

+

Keep your words fresh with reviews and challenges.

+
+ {/if} +
+
+
+

reviews

+

{due} due

+

+ {#if !p || !Object.keys(p.cards).length} + Words from finished lessons show up here when it's time to repeat them. + {:else if due} + Spaced repetition: review now to keep them. + {:else} + Nothing due. You can still practise your weakest words. + {/if} +

+
+ Review +
+
+ +
+
+

challenges

+ all words +
+ +

endless drills

+
+ {#each data.drills as d (d.id)} + {@const skill = Object.entries(p?.skills ?? {}).filter( + ([k]) => k === d.id || k.startsWith(d.id + '-') + )} + {@const seen = skill.reduce((n, [, s]) => n + s.seen, 0)} + {@const right = skill.reduce((n, [, s]) => n + s.right, 0)} + +

{d.title}

+

{d.description}

+ {#if seen}

+ {seen} answered · {Math.round((right / seen) * 100)}% right +

{/if} +
+ {/each} +
+
+ +
+

lessons

+
+ {#each course.levels as lv (lv.id)} + {@const done = lv.lessons.filter((l) => p?.lessons[l.id]).length} +
+
+
+

+ {lv.band}{lv.title} +

+

{lv.description}

+
+ +
+
+ +
+
    + {#each lv.lessons as lesson, i (lesson.id)} + {@const record = p?.lessons[lesson.id]} +
  1. + + + {#if record}{:else}{i + 1}{/if} + + + {lesson.title} + {lesson.summary} + + {#if record}{record.best}%{/if} + +
  2. + {/each} +
+
+ {/each} +
+
diff --git a/src/routes/projects/language-learner/[lang]/+page.ts b/src/routes/projects/language-learner/[lang]/+page.ts new file mode 100644 index 0000000..e46de90 --- /dev/null +++ b/src/routes/projects/language-learner/[lang]/+page.ts @@ -0,0 +1,11 @@ +import type { PageLoad } from './$types'; + +export const load: PageLoad = async ({ parent }) => { + const { course } = await parent(); + return { + seo: { + title: `Learn ${course.name} · Language Learner`, + description: `${course.name} lessons from ${course.levels[0].band} to ${course.levels.at(-1)!.band}, with spaced repetition and endless daily challenges.` + } + }; +}; diff --git a/src/routes/projects/language-learner/[lang]/lesson/[lesson]/+page.svelte b/src/routes/projects/language-learner/[lang]/lesson/[lesson]/+page.svelte new file mode 100644 index 0000000..62e37bd --- /dev/null +++ b/src/routes/projects/language-learner/[lang]/lesson/[lesson]/+page.svelte @@ -0,0 +1,173 @@ + + +{#if queue} + {#key run} + + {/key} +{:else} + + + {#if result} + + + {#if data.next} + Next: {data.next.title} + {:else} + Back to {course.name} + {/if} + + {/if} + +
+
+

+ {data.level.band}{data.level.title} +

+

{lesson.title}

+

{lesson.summary}

+
+
+ {#if record}best {record.best}% · {record.runs}×{/if} + +
+
+ +
+
+ + {@html data.notesHtml} +
+ + +
+{/if} diff --git a/src/routes/projects/language-learner/[lang]/lesson/[lesson]/+page.ts b/src/routes/projects/language-learner/[lang]/lesson/[lesson]/+page.ts new file mode 100644 index 0000000..b617c7f --- /dev/null +++ b/src/routes/projects/language-learner/[lang]/lesson/[lesson]/+page.ts @@ -0,0 +1,24 @@ +import { error } from '@sveltejs/kit'; +import { marked } from 'marked'; +import { allLessons } from '$lib/language-learner/course'; +import type { PageLoad } from './$types'; + +export const load: PageLoad = async ({ parent, params }) => { + const { course } = await parent(); + const lessons = allLessons(course); + const index = lessons.findIndex((l) => l.id === params.lesson); + if (index < 0) error(404, 'Unknown lesson'); + const lesson = lessons[index]; + const level = course.levels.find((lv) => lv.id === lesson.level)!; + return { + lesson, + level, + next: lessons[index + 1] ?? null, + // Notes are course content written in this repository, not user input. + notesHtml: marked.parse(lesson.notes, { async: false }), + seo: { + title: `${lesson.title} · ${course.name} ${level.band}`, + description: `${course.name} lesson: ${lesson.summary}. ${lesson.words.length} words with exercises.` + } + }; +}; diff --git a/src/routes/projects/language-learner/[lang]/practice/[mode]/+page.svelte b/src/routes/projects/language-learner/[lang]/practice/[mode]/+page.svelte new file mode 100644 index 0000000..47fc8a3 --- /dev/null +++ b/src/routes/projects/language-learner/[lang]/practice/[mode]/+page.svelte @@ -0,0 +1,346 @@ + + +{#if setup} + {#key run} + + {/key} +{:else} + + + {#if result && headline} + + {#if headline.note}

{headline.note}

{/if} + + Back to {course.name} +
+ {/if} + +
+

{mode.title}

+

{mode.description}

+
+ + {#if progress.loaded} +
+ {#if mixedMode} +
+

Questions from

+
+ + {#each course.levels as lv (lv.id)} + + {/each} + +
+

+ {scope.lessons.length} lessons: {scope.lessons + .slice(0, 4) + .map((l) => l.title) + .join(', ')}{scope.lessons.length > 4 ? ', …' : ''} +

+
+ {/if} + + {#if mode.kind === 'drill'} +
+

Difficulty

+
+ {#each [1, 2, 3, 4, 5] as n (n)} + + {/each} +
+
+ {/if} + + {#if mode.kind === 'review' && reviewInfo} +

+ {#if !reviewInfo.known} + No words to review yet. Finish a lesson first. + {:else} + {reviewInfo.due} due now, {reviewInfo.known} words learned. + {reviewInfo.due + ? 'Due words come first; after that you keep practising your weakest words.' + : 'Nothing is due, so this practises your weakest words.'} + {/if} +

+ {/if} + +
+ + {#if dailyDone !== undefined} + Done today: {dailyDone}% + {:else if best !== undefined && mode.kind !== 'daily'} + Best: {best} + {/if} + {#if mode.kind === 'survival' || mode.kind === 'endless' || mode.kind === 'drill'} + Endless: stop any time with ✕. + {/if} +
+
+ {/if} +{/if} diff --git a/src/routes/projects/language-learner/[lang]/practice/[mode]/+page.ts b/src/routes/projects/language-learner/[lang]/practice/[mode]/+page.ts new file mode 100644 index 0000000..1c78bc1 --- /dev/null +++ b/src/routes/projects/language-learner/[lang]/practice/[mode]/+page.ts @@ -0,0 +1,37 @@ +import { error } from '@sveltejs/kit'; +import { MODES } from '$lib/language-learner/modes'; +import type { PageLoad } from './$types'; + +export type Mode = + | { + kind: 'daily' | 'sprint' | 'survival' | 'endless' | 'review'; + title: string; + description: string; + } + | { kind: 'drill'; id: string; title: string; description: string } + | { kind: 'test'; level: string; title: string; description: string }; + +export const load: PageLoad = async ({ parent, params }) => { + const { course, drills } = await parent(); + let mode: Mode | undefined; + const known = MODES.find((m) => m.id === params.mode); + if (known) mode = { kind: known.id, title: known.title, description: known.description }; + const drill = + params.mode.startsWith('drill-') && drills.find((d) => d.id === params.mode.slice(6)); + if (drill) + mode = { kind: 'drill', id: drill.id, title: drill.title, description: drill.description }; + const level = + params.mode.startsWith('test-') && course.levels.find((lv) => lv.id === params.mode.slice(5)); + if (level) + mode = { + kind: 'test', + level: level.id, + title: `${level.band} level test`, + description: `20 questions from ${level.band} (${level.title}). Score 80% or more to mark every ${level.band} lesson as done.` + }; + if (!mode) error(404, 'Unknown practice mode'); + return { + mode, + seo: { title: `${mode.title} · ${course.name}`, description: mode.description, index: false } + }; +}; diff --git a/src/routes/projects/language-learner/[lang]/words/+page.svelte b/src/routes/projects/language-learner/[lang]/words/+page.svelte new file mode 100644 index 0000000..ccb3b53 --- /dev/null +++ b/src/routes/projects/language-learner/[lang]/words/+page.svelte @@ -0,0 +1,139 @@ + + + + +
+

{course.name} words

+

+ {#each ['learning', 'familiar', 'known', 'mastered'] as const as m (m)} + {counts[m]} {m} + {/each} + {counts.new} new +

+
+ +
+ + {#each filters as f (f.id)} + + {/each} +
+ +

{visible.length} shown

+
    + {#each visible.slice(0, 400) as r (r.entry.id)} +
  • + + + {r.entry.target.join(' / ')} + {#if code === 'ko' && progress.settings.romanization && r.entry.kind !== 'script'} + {romanize(r.entry.target[0])} + {/if} + {r.entry.gloss.join(', ')} + + + {#if voice && progress.settings.audio}{/if} +
  • + {/each} +
+{#if visible.length > 400}

+ Showing the first 400. Search to narrow it down. +

{/if} diff --git a/src/routes/projects/language-learner/[lang]/words/+page.ts b/src/routes/projects/language-learner/[lang]/words/+page.ts new file mode 100644 index 0000000..e63f642 --- /dev/null +++ b/src/routes/projects/language-learner/[lang]/words/+page.ts @@ -0,0 +1,11 @@ +import type { PageLoad } from './$types'; + +export const load: PageLoad = async ({ parent }) => { + const { course } = await parent(); + return { + seo: { + title: `${course.name} vocabulary · Language Learner`, + description: `Every word and sentence in the ${course.name} course, with translations.` + } + }; +}; diff --git a/src/routes/projects/language-learner/language-learner.css b/src/routes/projects/language-learner/language-learner.css new file mode 100644 index 0000000..4a88c52 --- /dev/null +++ b/src/routes/projects/language-learner/language-learner.css @@ -0,0 +1,450 @@ +.ll { + --ll-right: #3fb872; + --ll-right-bg: rgba(63, 184, 114, 0.12); + --ll-wrong: #e5645c; + --ll-wrong-bg: rgba(229, 100, 92, 0.12); + --ll-gold: #e8b64c; +} + +:root[data-theme='light'] .ll { + --ll-right: #16804a; + --ll-right-bg: rgba(22, 128, 74, 0.08); + --ll-wrong: #c23a33; + --ll-wrong-bg: rgba(194, 58, 51, 0.08); + --ll-gold: #a86f00; +} + +.ll :lang(ko) { + font-family: 'Apple SD Gothic Neo', 'Noto Sans KR', 'Malgun Gothic', var(--font-body), sans-serif; + word-break: keep-all; +} + +.ll-card { + border: 1px solid var(--site-border); + border-radius: 0.75rem; + background: var(--site-surface); + padding: 1.25rem; +} + +a.ll-card:hover, +button.ll-card:hover:not(:disabled) { + background: var(--site-hover); +} + +.ll-button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.4rem; + border-radius: 0.5rem; + background: var(--site-text); + color: var(--site-bg); + padding: 0.55rem 1.1rem; + font-weight: 600; + white-space: nowrap; +} +.ll-button:hover:not(:disabled) { + opacity: 0.85; +} +.ll-button:disabled { + opacity: 0.35; + cursor: not-allowed; +} +.ll-button-quiet { + background: transparent; + color: var(--site-text); + border: 1px solid var(--site-border); +} +.ll-button-quiet:hover:not(:disabled) { + background: var(--site-hover); + opacity: 1; +} + +.ll-icon-button, +.ll-speak { + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--site-muted); + border-radius: 999px; +} +.ll-icon-button { + width: 2.25rem; + height: 2.25rem; +} +.ll-speak { + width: 1.9rem; + height: 1.9rem; + vertical-align: middle; +} +.ll-icon-button:hover, +.ll-speak:hover { + color: var(--site-text); + background: var(--site-hover); +} + +.ll-pill { + display: inline-flex; + align-items: center; + gap: 0.3rem; + border: 1px solid var(--site-border); + border-radius: 999px; + padding: 0.15rem 0.6rem; + font-size: 0.8125rem; + color: var(--site-muted); + white-space: nowrap; +} +.ll-combo { + color: var(--ll-gold); + border-color: var(--ll-gold); +} +.ll-heart { + color: var(--ll-wrong); + fill: var(--ll-wrong); +} +.ll-heart-lost { + opacity: 0.3; +} + +.ll-chip { + display: inline-flex; + align-items: center; + gap: 0.4rem; + border: 1px solid var(--site-border); + border-radius: 999px; + padding: 0.3rem 0.8rem; + font-size: 0.875rem; + color: var(--site-muted); +} +.ll-chip:hover:not(:disabled) { + background: var(--site-hover); + color: var(--site-text); +} +.ll-chip[aria-pressed='true'] { + border-color: var(--site-text); + color: var(--site-text); +} + +.ll-muted { + color: var(--site-muted); +} + +.ll-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 2.25rem; + border-radius: 0.4rem; + border: 1px solid var(--site-border); + padding: 0.1rem 0.4rem; + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.04em; + color: var(--site-muted); +} + +.ll-done { + color: var(--ll-right); +} + +/* session */ +.ll-session { + max-width: 42rem; + margin: 0 auto; +} +.ll-session-top { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 2rem; +} +.ll-stat-line { + font-size: 0.875rem; + color: var(--site-muted); +} +.ll-exercise { + min-height: 18rem; +} +.ll-instruction { + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--site-muted); + margin-bottom: 1rem; +} +.ll-prompt { + margin-bottom: 1.5rem; +} +.ll-prompt-text { + font-size: 1.75rem; + font-weight: 600; + line-height: 1.3; +} +.ll-roman { + display: block; + font-size: 0.875rem; + font-weight: 400; + color: var(--site-muted); +} +.ll-context { + margin-top: 0.5rem; + color: var(--site-muted); +} +.ll-hint { + font-size: 0.8125rem; + color: var(--site-muted); +} +.ll-listen { + display: inline-flex; + align-items: center; + justify-content: center; + width: 5rem; + height: 5rem; + border-radius: 1rem; + background: var(--site-text); + color: var(--site-bg); +} +.ll-slow { + margin-left: 0.75rem; + font-size: 0.875rem; + color: var(--site-muted); + text-decoration: underline; + text-underline-offset: 3px; +} + +.ll-options { + display: grid; + gap: 0.6rem; +} +@media (min-width: 640px) { + .ll-options { + grid-template-columns: 1fr 1fr; + } + .ll-options.ll-options-wide { + grid-template-columns: 1fr; + } +} +.ll-option { + display: flex; + align-items: center; + gap: 0.75rem; + width: 100%; + border: 1px solid var(--site-border); + border-radius: 0.6rem; + padding: 0.75rem 1rem; + text-align: left; + font-size: 1.0625rem; + background: var(--site-bg); + transition: background 0.1s; +} +.ll-option:hover:not(:disabled) { + background: var(--site-hover); +} +.ll-option kbd { + font-family: var(--font-mono); + font-size: 0.75rem; + color: var(--site-muted); + border: 1px solid var(--site-border); + border-radius: 0.3rem; + padding: 0 0.35rem; +} +.ll-option.is-glyph span { + font-size: 1.75rem; + line-height: 1.2; +} +.ll-option.is-right { + border-color: var(--ll-right); + background: var(--ll-right-bg); +} +.ll-option.is-wrong { + border-color: var(--ll-wrong); + background: var(--ll-wrong-bg); +} +.ll-option.is-dim { + opacity: 0.5; +} +.ll-option.is-selected { + border-color: var(--site-text); +} +.ll-option.is-matched { + border-color: var(--ll-right); + background: var(--ll-right-bg); + opacity: 0.5; +} + +.ll-type { + display: flex; + gap: 0.5rem; + margin-bottom: 0.5rem; +} +.ll-type input, +.ll-input { + flex: 1; + min-width: 0; + border: 1px solid var(--site-border); + border-radius: 0.5rem; + background: var(--site-bg); + padding: 0.65rem 0.9rem; + font-size: 1.125rem; +} +.ll-input { + font-size: 1rem; + padding: 0.45rem 0.75rem; +} + +.ll-build-line { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem; + min-height: 3.5rem; + border-bottom: 2px solid var(--site-border); + padding: 0.5rem 0; + margin-bottom: 1rem; +} +.ll-bank { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 1.25rem; +} +.ll-tile { + border: 1px solid var(--site-border); + border-bottom-width: 3px; + border-radius: 0.5rem; + background: var(--site-bg); + padding: 0.35rem 0.8rem; + font-size: 1.0625rem; +} +.ll-tile:hover:not(:disabled) { + background: var(--site-hover); +} +.ll-tile.is-used { + visibility: hidden; +} +.ll-actions { + display: flex; + justify-content: flex-end; +} + +.ll-match { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.6rem; +} +.ll-match > div { + display: grid; + gap: 0.6rem; + align-content: start; +} +.ll-match.is-shaking { + animation: ll-shake 0.3s; +} +@keyframes ll-shake { + 25% { + transform: translateX(-4px); + } + 75% { + transform: translateX(4px); + } +} + +.ll-feedback { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + margin-top: 1.5rem; + border-radius: 0.75rem; + padding: 1rem 1.25rem; + border: 1px solid; +} +.ll-feedback.is-right { + border-color: var(--ll-right); + background: var(--ll-right-bg); + color: var(--ll-right); +} +.ll-feedback.is-wrong { + border-color: var(--ll-wrong); + background: var(--ll-wrong-bg); + color: var(--ll-wrong); +} +.ll-feedback p { + overflow-wrap: anywhere; +} +.ll-explain { + margin-top: 0.25rem; + color: var(--site-text); + font-size: 0.9375rem; +} +.ll-keys { + margin-top: 2rem; + text-align: center; + font-size: 0.75rem; + color: var(--site-muted); +} +@media (hover: none) { + .ll-keys, + .ll-option kbd { + display: none; + } +} + +/* lesson notes */ +.ll-notes { + --tw-prose-body: var(--site-text); + --tw-prose-headings: var(--site-text); + --tw-prose-bold: var(--site-text); + --tw-prose-bullets: var(--site-muted); + --tw-prose-th-borders: var(--site-border); + --tw-prose-td-borders: var(--site-border); + --tw-prose-code: var(--site-text); + max-width: none; +} +.ll-notes table { + display: block; + overflow-x: auto; +} + +/* level path */ +.ll-lesson-dot { + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: 2rem; + height: 2rem; + border-radius: 999px; + border: 1px solid var(--site-border); + font-size: 0.8125rem; + color: var(--site-muted); +} +.ll-lesson-dot.is-done { + border-color: var(--ll-right); + background: var(--ll-right-bg); + color: var(--ll-right); +} +.ll-lesson-dot.is-next { + border-color: var(--site-text); + color: var(--site-text); +} + +.ll-mastery { + display: inline-block; + width: 0.6rem; + height: 0.6rem; + border-radius: 999px; + background: var(--site-border); +} +.ll-mastery[data-level='learning'] { + background: var(--ll-wrong); +} +.ll-mastery[data-level='familiar'] { + background: var(--ll-gold); +} +.ll-mastery[data-level='known'] { + background: #5aa0e0; +} +.ll-mastery[data-level='mastered'] { + background: var(--ll-right); +} diff --git a/src/routes/sitemap.xml/+server.ts b/src/routes/sitemap.xml/+server.ts index 412412a..074d576 100644 --- a/src/routes/sitemap.xml/+server.ts +++ b/src/routes/sitemap.xml/+server.ts @@ -2,6 +2,7 @@ import { db } from '$lib/server/db'; import { blogArticles } from '$lib/server/db/schema'; import { SITE_ORIGIN } from '$lib/seo'; import { races } from '$lib/derby/races'; +import { COURSES } from '$lib/language-learner/courses'; import { eq } from 'drizzle-orm'; import type { RequestHandler } from './$types'; @@ -14,7 +15,9 @@ const staticPaths = [ '/projects/animaldex', '/projects/derby', '/projects/spotify-beatmap-finder', - '/projects/world-opinions' + '/projects/world-opinions', + '/projects/language-learner', + ...COURSES.map((c) => `/projects/language-learner/${c.code}`) ]; function escapeXml(value: string): string {