add language learner project
Some checks failed
Test and deploy staging / staging (push) Has been cancelled

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 <noreply@anthropic.com>
This commit is contained in:
2026-09-24 17:34:56 +00:00
parent afb1f191df
commit 438623d857
69 changed files with 12669 additions and 4 deletions

26
README.md Executable file → Normal file
View File

@@ -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/<lang>/<level>.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.

2
eslint.config.js Executable file → Normal file
View File

@@ -20,7 +20,7 @@ export default [
}
},
{
files: ['**/*.svelte'],
files: ['**/*.svelte', '**/*.svelte.ts'],
languageOptions: {
parserOptions: {
parser: ts.parser

View File

@@ -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);
});
});

View File

@@ -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 };
}

View File

@@ -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 }));
});
});

View File

@@ -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<Course, 'levels'>, levels: ReturnType<typeof level>[]): 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);
}

View File

@@ -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
`
})
]
);

View File

@@ -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
`
})
]
);

View File

@@ -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
`
})
]
);

View File

@@ -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
`
})
]
);

View File

@@ -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
`
})
]
);

View File

@@ -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]
);

View File

@@ -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<LangCode, () => Promise<{ default: Course }>> = {
nl: () => import('./nl'),
ko: () => import('./ko'),
en: () => import('./en')
};
export async function loadCourse(code: LangCode): Promise<Course> {
return (await loaders[code]()).default;
}

View File

@@ -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: 버스___ 타요. | 를 | 에; 로; 가
`
})
]
);

View File

@@ -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.
`
})
]
);

View File

@@ -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: 화가 ___. | 났어요 | 했어요; 나요; 있었어요
`
})
]
);

View File

@@ -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 ___ | 나무에서 | 산에서; 하늘에서; 집에서
`
})
]
);

View File

@@ -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): | 십중팔구 | 일석이조; 유비무환; 대기만성
`
})
]
);

View File

@@ -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
`
})
]
);

View File

@@ -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]
);

View File

@@ -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.
`
})
]
);

View File

@@ -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
`
})
]
);

View File

@@ -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
`
})
]
);

View File

@@ -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
`
})
]
);

View File

@@ -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
`
})
]
);

View File

@@ -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]
);

View File

@@ -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
});
}
}
];

View File

@@ -0,0 +1,14 @@
import type { LangCode } from '../types';
import type { DrillMode } from './shared';
export type { DrillMode, DrillContext } from './shared';
const loaders: Record<LangCode, () => Promise<{ drills: DrillMode[] }>> = {
nl: () => import('./nl'),
ko: () => import('./ko'),
en: () => import('./en')
};
export async function loadDrills(code: LangCode): Promise<DrillMode[]> {
return (await loaders[code]()).drills;
}

View File

@@ -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
});
}
}
];

View File

@@ -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<number, string> = {
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
});
}
}
];

View File

@@ -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))
];

View File

@@ -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]));
}

View File

@@ -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('의사예요');
});
});

View File

@@ -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<string, string> = {
ㄱ: 'g',
ㄲ: 'kk',
ㄴ: 'n',
ㄷ: 'd',
ㄸ: 'tt',
ㄹ: 'r',
ㅁ: 'm',
ㅂ: 'b',
ㅃ: 'pp',
ㅅ: 's',
ㅆ: 'ss',
ㅇ: '',
ㅈ: 'j',
ㅉ: 'jj',
ㅊ: 'ch',
ㅋ: 'k',
ㅌ: 't',
ㅍ: 'p',
ㅎ: 'h'
};
const ROMAN_MEDIAL: Record<string, string> = {
ㅏ: '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<string, string> = {
ㄱ: '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<string, [string, string]> = {
ㄳ: ['ㄱ', 'ㅅ'],
ㄵ: ['ㄴ', 'ㅈ'],
ㄶ: ['', 'ㄴ'],
ㄺ: ['ㄹ', 'ㄱ'],
ㄻ: ['ㄹ', 'ㅁ'],
ㄼ: ['ㄹ', 'ㅂ'],
ㄽ: ['ㄹ', 'ㅅ'],
ㄾ: ['ㄹ', 'ㅌ'],
ㄿ: ['ㄹ', 'ㅍ'],
ㅀ: ['', 'ㄹ'],
ㅄ: ['ㅂ', 'ㅅ']
};
const ASPIRATED: Record<string, string> = {
k: 'ㅋ',
t: 'ㅌ',
p: 'ㅍ',
ㄱ: 'ㅋ',
ㄷ: 'ㅌ',
ㅈ: 'ㅊ'
};
const NASAL: Record<string, string> = { 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<Particle, [string, string]> = {
topic: ['은', '는'],
subject: ['이', '가'],
object: ['을', '를'],
and: ['과', '와'],
with: ['이랑', '랑'],
direction: ['으로', '로'],
copula: ['이에요', '예요']
};
if (kind === 'direction' && last?.final === 'ㄹ') return noun + '로';
return noun + forms[kind][batchim ? 0 : 1];
}

View File

@@ -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(/[가-힣]/);
});
});

View File

@@ -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<KoreanVerb, 'dict' | 'irr'>): 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<string, string> = { ㅗ: 'ㅘ', ㅜ: 'ㅝ', ㅣ: 'ㅕ', ㅚ: 'ㅙ' };
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<KoreanVerb, 'dict' | 'irr'>): 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<string, string> = {
먹다: '드세요',
마시다: '드세요',
자다: '주무세요',
있다: '계세요',
말하다: '말씀하세요'
};
export type KoreanForm =
| 'present'
| 'past'
| 'future'
| 'formal'
| 'want'
| 'progressive'
| 'negative'
| 'can'
| 'request'
| 'casual';
export const FORM_LABELS: Record<KoreanForm, string> = {
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
`
)
];

View File

@@ -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'
}
];

View File

@@ -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('네');
});
});

View File

@@ -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<number, string> = { 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];
}

View File

@@ -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<string, LessonRecord>;
cards: Record<string, Card>;
xp: number;
/** XP earned per local day, `YYYY-MM-DD`. */
days: Record<string, number>;
/** First daily-challenge score per day, as a percentage. */
daily: Record<string, number>;
/** Best score per challenge mode. */
bests: Record<string, number>;
/** Accuracy per endless drill. */
skills: Record<string, { seen: number; right: number }>;
/** Level last chosen for mixed challenges; empty means "what I have learned". */
scope: string;
};
export type Settings = {
typing: Partial<Record<LangCode, boolean>>;
audio: boolean;
romanization: boolean;
/** Daily XP goal. */
goal: number;
};
type Store = { version: 1; langs: Partial<Record<LangCode, LangProgress>>; 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<string, number>): 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<Store>(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<string, number> = {};
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();

View File

@@ -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');
});
});

View File

@@ -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 = <T>(rng: Rng, list: readonly T[]): T => list[Math.floor(rng() * list.length)];
export function shuffle<T>(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<T>(
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;
}

View File

@@ -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<SpeechSynthesisVoice[]> {
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<boolean> {
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);
}

View File

@@ -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');
});
});

View File

@@ -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);

View File

@@ -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, ' ');

View File

@@ -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';
};

View File

@@ -0,0 +1,294 @@
<script lang="ts">
import { onMount } from 'svelte';
import Volume2 from 'lucide-svelte/icons/volume-2';
import { romanize } from '../hangul';
import { speak } from '../speech';
import { grade, normalize } from '../text';
import type { Answer, Exercise, LangCode } from '../types';
import Speak from './Speak.svelte';
let {
exercise,
code,
glossLang,
voice,
audio,
romanization,
locked,
onanswer
}: {
exercise: Exercise;
code: LangCode;
glossLang: LangCode;
voice: string;
audio: boolean;
romanization: boolean;
locked: boolean;
onanswer: (answer: Answer) => void;
} = $props();
// Romanization would give away Hangul reading questions and grammar drills.
const showRoman = $derived(
code === 'ko' &&
romanization &&
!exercise.skill &&
!exercise.key.startsWith('drill:') &&
!exercise.entries.some((e) => e.kind === 'script')
);
const roman = (text: string) => (showRoman && /[가-힣]/.test(text) ? romanize(text) : '');
const langOf = (side: 'target' | 'gloss') => (side === 'target' ? code : glossLang);
let chosen = $state<string | null>(null);
let typed = $state('');
let picked = $state<number[]>([]);
let input = $state<HTMLInputElement>();
// match state
let left = $state<number | null>(null);
let right = $state<number | null>(null);
let matched = $state<number[]>([]);
let matchedRight = $state<number[]>([]);
let wrongFlash = $state(false);
let mistakes = $state(0);
const rightOrder = $derived.by(() => {
if (exercise.type !== 'match') return [];
// deterministic shuffle so the columns don't line up
return exercise.pairs.map((_, i) => i).sort((a, b) => ((a * 7 + 3) % 11) - ((b * 7 + 3) % 11));
});
onMount(() => {
if (exercise.listen && exercise.audio && audio) speak(exercise.audio, voice);
input?.focus();
});
function choose(option: string) {
if (locked || exercise.type !== 'choose') return;
chosen = option;
onanswer({ correct: option === exercise.answer, given: option });
}
function submitTyped() {
if (locked || exercise.type !== 'type' || !typed.trim()) return;
const { verdict, match } = grade(typed, exercise.accept);
onanswer({
correct: verdict !== 'wrong',
given: typed,
note:
verdict === 'accent'
? `Mind the accents: ${match}`
: verdict === 'typo'
? `Almost! Watch the spelling: ${match}`
: undefined
});
}
function submitBuilt() {
if (locked || exercise.type !== 'build' || !picked.length) return;
const given = picked.map((i) => exercise.tiles[i]).join(' ');
const accepted = [exercise.answer, ...exercise.entries.flatMap((e) => e.target)];
onanswer({ correct: accepted.some((a) => normalize(a) === normalize(given)), given });
}
function pickPair(side: 'left' | 'right', index: number) {
if (locked || exercise.type !== 'match') return;
if (side === 'left') left = index;
else right = index;
if (left === null || right === null) return;
// Compare texts rather than positions, in case two pairs look alike.
if (exercise.pairs[left].gloss === exercise.pairs[right].gloss) {
matched = [...matched, left];
matchedRight = [...matchedRight, right];
if (matched.length === exercise.pairs.length) onanswer({ correct: mistakes <= 1, given: '' });
} else {
mistakes++;
wrongFlash = true;
setTimeout(() => (wrongFlash = false), 350);
}
left = null;
right = null;
}
function onkeydown(event: KeyboardEvent) {
if (locked || event.defaultPrevented || event.target instanceof HTMLInputElement) return;
if (exercise.type === 'choose' && /^[1-9]$/.test(event.key)) {
const option = exercise.options[Number(event.key) - 1];
if (option) {
event.preventDefault();
choose(option);
}
}
if (exercise.type === 'build') {
if (event.key === 'Enter') {
event.preventDefault();
submitBuilt();
} else if (event.key === 'Backspace') {
event.preventDefault();
picked = picked.slice(0, -1);
}
}
}
const optionState = (option: string) => {
if (exercise.type !== 'choose' || !locked) return '';
if (option === exercise.answer) return 'is-right';
if (option === chosen) return 'is-wrong';
return 'is-dim';
};
</script>
<svelte:window {onkeydown} />
<div class="ll-exercise">
<p class="ll-instruction">{exercise.instruction}</p>
{#if exercise.type === 'choose' || exercise.type === 'type'}
<div class="ll-prompt">
{#if exercise.listen && exercise.audio && audio}
<button
type="button"
class="ll-listen"
aria-label="Play again"
onclick={() => speak(exercise.audio!, voice)}
><Volume2 size={36} aria-hidden="true" /></button
>
<button type="button" class="ll-slow" onclick={() => speak(exercise.audio!, voice, 0.6)}
>slower</button
>
{:else}
<p class="ll-prompt-text" lang={langOf(exercise.promptLang)}>
{exercise.prompt}
{#if exercise.audio && audio && exercise.promptLang === 'target'}
<Speak text={exercise.audio} {voice} />
{/if}
</p>
{#if exercise.promptLang === 'target' && roman(exercise.prompt)}
<p class="ll-roman">{roman(exercise.prompt)}</p>
{/if}
{/if}
{#if exercise.context}
<p class="ll-context">{exercise.context}</p>
{/if}
</div>
{/if}
{#if exercise.type === 'choose'}
<div class="ll-options" class:ll-options-wide={exercise.options.some((o) => o.length > 28)}>
{#each exercise.options as option, i (option)}
<button
type="button"
class="ll-option {optionState(option)}"
class:is-glyph={[...option].length <= 2 && /[\u3131-\u318e가-힣]/.test(option)}
disabled={locked}
onclick={() => choose(option)}
>
<kbd>{i + 1}</kbd>
<span lang={langOf(exercise.optionLang)}>
{option}
{#if exercise.optionLang === 'target' && roman(option)}
<small class="ll-roman">{roman(option)}</small>
{/if}
</span>
</button>
{/each}
</div>
{:else if exercise.type === 'type'}
<form
class="ll-type"
onsubmit={(event) => {
event.preventDefault();
submitTyped();
}}
>
<input
bind:this={input}
bind:value={typed}
lang={langOf(exercise.answerLang)}
disabled={locked}
autocomplete="off"
autocapitalize="off"
spellcheck="false"
aria-label="Your answer"
placeholder={exercise.answerLang === 'target'
? `Type in ${code === 'ko' ? 'Korean' : code === 'nl' ? 'Dutch' : 'English'}`
: 'Type your answer'}
onkeydown={(event) => {
if (event.key === 'Enter') {
event.preventDefault();
submitTyped();
}
}}
/>
<button type="submit" class="ll-button" disabled={locked || !typed.trim()}>Check</button>
</form>
{#if code === 'ko' && exercise.answerLang === 'target'}
<p class="ll-hint">
Needs a Korean keyboard. Turn typing off in the settings if you don't have one.
</p>
{/if}
{:else if exercise.type === 'build'}
<div class="ll-prompt">
<p class="ll-prompt-text" lang={glossLang}>{exercise.prompt}</p>
</div>
<div class="ll-build-line" lang={code} aria-label="Your sentence">
{#each picked as tileIndex, i (tileIndex)}
<button
type="button"
class="ll-tile"
disabled={locked}
onclick={() => (picked = picked.filter((_, j) => j !== i))}
>{exercise.tiles[tileIndex]}</button
>
{:else}
<span class="ll-hint">Tap the words in the right order</span>
{/each}
</div>
<div class="ll-bank" lang={code}>
{#each exercise.tiles as tile, i (i)}
<button
type="button"
class="ll-tile"
class:is-used={picked.includes(i)}
disabled={locked || picked.includes(i)}
onclick={() => (picked = [...picked, i])}>{tile}</button
>
{/each}
</div>
<div class="ll-actions">
<button
type="button"
class="ll-button"
disabled={locked || !picked.length}
onclick={submitBuilt}>Check</button
>
</div>
{:else if exercise.type === 'match'}
<div class="ll-match" class:is-shaking={wrongFlash}>
<div>
{#each exercise.pairs as pair, i (i)}
<button
type="button"
class="ll-option"
class:is-selected={left === i}
class:is-matched={matched.includes(i)}
disabled={locked || matched.includes(i)}
lang={code}
onclick={() => pickPair('left', i)}>{pair.target}</button
>
{/each}
</div>
<div>
{#each rightOrder as i (i)}
<button
type="button"
class="ll-option"
class:is-selected={right === i}
class:is-matched={matchedRight.includes(i)}
disabled={locked || matchedRight.includes(i)}
lang={glossLang}
onclick={() => pickPair('right', i)}>{exercise.pairs[i].gloss}</button
>
{/each}
</div>
</div>
{/if}
</div>

View File

@@ -0,0 +1,79 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import type { LangCode, SessionResult } from '../types';
let {
result,
code,
glossLang,
title,
subtitle,
score,
percent = false,
record = false,
children
}: {
result: SessionResult;
code: LangCode;
glossLang: LangCode;
title: string;
subtitle?: string;
/** Headline number; defaults to accuracy. */
score?: number;
/** Show the score as a percentage. */
percent?: boolean;
record?: boolean;
children?: Snippet;
} = $props();
const accuracy = $derived(
result.answered ? Math.round((result.correct / result.answered) * 100) : 0
);
const unique = $derived([...new Map(result.mistakes.map((m) => [m.key, m])).values()]);
function answerOf(m: SessionResult['mistakes'][number]) {
if (m.type === 'choose') return m.answer;
if (m.type === 'type') return m.accept[0];
if (m.type === 'build') return m.answer;
return '';
}
</script>
<section class="ll-card mb-10" aria-live="polite">
<p class="text-sm ll-muted">{subtitle}</p>
<h2 class="mb-4 text-3xl font-semibold">{title}{record ? ' · new best!' : ''}</h2>
<div class="mb-6 grid grid-cols-3 gap-3 text-center">
<div>
<p class="text-3xl font-semibold">
{score ?? accuracy}{score === undefined || percent ? '%' : ''}
</p>
<p class="text-sm ll-muted">{score === undefined ? 'accuracy' : 'score'}</p>
</div>
<div>
<p class="text-3xl font-semibold">{result.correct}/{result.answered}</p>
<p class="text-sm ll-muted">right</p>
</div>
<div>
<p class="text-3xl font-semibold">+{result.xp}</p>
<p class="text-sm ll-muted">XP</p>
</div>
</div>
{#if unique.length}
<h3 class="mb-2 font-semibold">to look at again</h3>
<ul
class="mb-6 divide-y divide-[var(--site-border)] border-y border-[var(--site-border)] text-sm"
>
{#each unique.slice(0, 12) as m (m.key)}
<li class="flex flex-wrap justify-between gap-x-4 py-2">
<span lang={m.type === 'build' ? glossLang : undefined}
>{m.type === 'match' ? 'Matching pairs' : m.prompt}</span
>
<strong lang={code}>{answerOf(m)}</strong>
</li>
{/each}
</ul>
{/if}
<div class="flex flex-wrap gap-2">
{@render children?.()}
</div>
</section>

View File

@@ -0,0 +1,246 @@
<script lang="ts">
import { onDestroy, onMount, untrack } from 'svelte';
import Heart from 'lucide-svelte/icons/heart';
import Timer from 'lucide-svelte/icons/timer';
import X from 'lucide-svelte/icons/x';
import ProgressBar from '$lib/components/ProgressBar.svelte';
import { progress } from '../progress.svelte';
import { speak } from '../speech';
import type { Answer, Course, Exercise, SessionResult } from '../types';
import ExerciseView from './Exercise.svelte';
let {
course,
queue: initial,
more,
requeue = false,
lives,
seconds,
xpPer = 2,
audio,
onfinish
}: {
course: Course;
queue: Exercise[];
/** Supplies another exercise when the queue runs out; makes the session endless. */
more?: () => Exercise | null;
/** Ask wrongly answered exercises again at the end. */
requeue?: boolean;
lives?: number;
seconds?: number;
xpPer?: number;
audio: boolean;
onfinish: (result: SessionResult) => void;
} = $props();
const code = $derived(course.code);
// The parent remounts the session for every run, so the starting props are all it needs.
const config = untrack(() => ({ queue: [...initial], more, lives, seconds }));
const queue = config.queue;
const firstCount = queue.length;
const attempted = new WeakSet<Exercise>();
let unique = 0;
const started = Date.now();
let current = $state<Exercise | null>(queue.shift() ?? config.more?.() ?? null);
let turn = $state(0);
let locked = $state(false);
let feedback = $state<(Answer & { expected: string }) | null>(null);
let answered = $state(0);
let correct = $state(0);
let firstTry = $state(0);
let xp = $state(0);
let combo = $state(0);
let requeued = $state(0);
let livesLeft = $state(config.lives ?? 0);
let timeLeft = $state(config.seconds ?? 0);
let mistakes: Exercise[] = [];
let finished = false;
let advanceTimer: ReturnType<typeof setTimeout> | undefined;
let clock: ReturnType<typeof setInterval> | undefined;
onMount(() => {
if (!current) finish('done');
if (seconds)
clock = setInterval(() => {
timeLeft = Math.max(0, seconds - Math.floor((Date.now() - started) / 1000));
if (timeLeft === 0) finish('time');
}, 250);
});
onDestroy(() => {
clearInterval(clock);
clearTimeout(advanceTimer);
});
function expectedOf(ex: Exercise): string {
if (ex.type === 'choose') return ex.answer;
if (ex.type === 'type') return ex.accept[0];
if (ex.type === 'build') return ex.answer;
return '';
}
function answer(result: Answer) {
if (!current || locked) return;
const ex = current;
const first = !attempted.has(ex);
if (first) unique++;
attempted.add(ex);
answered++;
locked = true;
feedback = { ...result, expected: expectedOf(ex) };
progress.answer(code, ex.entries, result.correct);
if (ex.skill) progress.skill(code, ex.skill, result.correct);
if (result.correct) {
correct++;
if (first) firstTry++;
combo++;
// a small bonus for streaks of right answers keeps grinding rewarding
const gained = xpPer + (combo > 0 && combo % 10 === 0 ? 5 : 0);
xp += gained;
progress.addXp(code, gained);
} else {
combo = 0;
mistakes.push(ex);
if (requeue) {
queue.push(ex);
requeued++;
}
if (lives) livesLeft--;
progress.save();
}
if (audio && ex.audio && !ex.listen) speak(ex.audio, course.voice);
if (result.correct && !result.note) advanceTimer = setTimeout(next, seconds ? 350 : 900);
}
function next() {
clearTimeout(advanceTimer);
if (finished) return;
if (lives && livesLeft <= 0) return finish('lives');
const upcoming = queue.shift() ?? more?.() ?? null;
if (!upcoming) return finish('done');
current = upcoming;
turn++;
locked = false;
feedback = null;
}
function finish(reason: SessionResult['reason']) {
if (finished) return;
finished = true;
clearInterval(clock);
clearTimeout(advanceTimer);
onfinish({
answered,
correct,
firstTry,
total: unique,
xp,
seconds: Math.round((Date.now() - started) / 1000),
mistakes,
reason
});
}
function onkeydown(event: KeyboardEvent) {
if (!locked || event.defaultPrevented) return;
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
next();
}
}
const endless = $derived(!!more);
const expectedLang = $derived(
current?.type === 'choose'
? current.optionLang
: current?.type === 'type'
? current.answerLang
: 'target'
);
</script>
<svelte:window {onkeydown} />
<div class="ll-session">
<div class="ll-session-top">
<button
type="button"
class="ll-icon-button"
aria-label="End session"
title="End session"
onclick={() => finish('ended')}><X size={20} aria-hidden="true" /></button
>
<div class="min-w-0 flex-1">
{#if seconds}
<ProgressBar value={timeLeft} max={seconds} label="Time left" />
{:else if !endless}
<ProgressBar value={correct} max={firstCount + requeued} label="Session progress" />
{:else}
<p class="ll-stat-line">{correct} right · {answered - correct} wrong</p>
{/if}
</div>
{#if seconds}
<span class="ll-pill"><Timer size={16} aria-hidden="true" /> {timeLeft}s</span>
{/if}
{#if lives}
<span class="ll-pill" aria-label="{livesLeft} lives left">
{#each Array.from({ length: lives }, (_, i) => i) as i (i)}
<Heart
size={16}
aria-hidden="true"
class={i < livesLeft ? 'll-heart' : 'll-heart-lost'}
/>
{/each}
</span>
{/if}
{#if combo >= 3}
<span class="ll-pill ll-combo">{combo} in a row</span>
{/if}
<span class="ll-pill">{xp} XP</span>
</div>
{#if current}
{#key turn}
<ExerciseView
exercise={current}
{code}
glossLang={course.glossLang}
voice={course.voice}
{audio}
romanization={progress.settings.romanization}
{locked}
onanswer={answer}
/>
{/key}
{/if}
{#if feedback}
<div
class="ll-feedback"
class:is-right={feedback.correct}
class:is-wrong={!feedback.correct}
role="status"
>
<div class="min-w-0">
<p class="font-semibold">
{feedback.correct ? (feedback.note ? 'Correct, almost' : 'Correct!') : 'Not quite'}
</p>
{#if feedback.note}
<p lang={code}>{feedback.note}</p>
{:else if !feedback.correct && feedback.expected}
<p>
Answer: <strong lang={expectedLang === 'target' ? code : course.glossLang}
>{feedback.expected}</strong
>
</p>
{/if}
{#if current?.explain && (!feedback.correct || feedback.note)}
<p class="ll-explain">{current.explain}</p>
{/if}
</div>
<button type="button" class="ll-button" onclick={next}>Continue</button>
</div>
{/if}
<p class="ll-keys">Keys: 1–4 to choose, Enter to check and continue.</p>
</div>

View File

@@ -0,0 +1,22 @@
<script lang="ts">
import Volume2 from 'lucide-svelte/icons/volume-2';
import { speak } from '../speech';
let {
text,
voice,
size = 18,
label = 'Listen'
}: { text: string; voice: string; size?: number; label?: string } = $props();
</script>
<button
type="button"
class="ll-speak"
aria-label={label}
title={label}
onclick={(event) => {
event.stopPropagation();
speak(text, voice);
}}><Volume2 {size} aria-hidden="true" /></button
>

View File

@@ -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',

View File

@@ -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<string, { title: string; description: string }> = {
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',

View File

@@ -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',

View File

@@ -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 },

View File

@@ -0,0 +1,13 @@
<script lang="ts">
import './language-learner.css';
import { onMount } from 'svelte';
import { progress } from '$lib/language-learner/progress.svelte';
let { children } = $props();
onMount(() => progress.load());
</script>
<div class="ll mx-auto max-w-6xl px-4 py-10 sm:px-6 sm:py-14">
{@render children()}
</div>

View File

@@ -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<string, { lessons: number; words: number; levels: string[] }>
};
};

View File

@@ -0,0 +1,245 @@
<script lang="ts">
import { resolve } from '$app/paths';
import ArrowRight from 'lucide-svelte/icons/arrow-right';
import Flame from 'lucide-svelte/icons/flame';
import ProgressBar from '$lib/components/ProgressBar.svelte';
import { COURSES } from '$lib/language-learner/courses';
import { progress } from '$lib/language-learner/progress.svelte';
import type { LangCode } from '$lib/language-learner/types';
let { data } = $props();
const goals = [20, 50, 100, 200];
let importError = $state('');
let fileInput = $state<HTMLInputElement>();
const completed = (code: LangCode) =>
Object.keys(progress.data.langs[code]?.lessons ?? {}).length;
function exportProgress() {
const blob = new Blob([progress.export()], { type: 'application/json' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `language-learner-${new Date().toISOString().slice(0, 10)}.json`;
link.click();
URL.revokeObjectURL(link.href);
}
async function importProgress(event: Event) {
const file = (event.currentTarget as HTMLInputElement).files?.[0];
if (!file) return;
try {
progress.import(await file.text());
importError = '';
} catch {
importError = 'That file is not a language learner backup.';
}
}
function reset(code: LangCode, name: string) {
if (confirm(`Delete all ${name} progress? This can't be undone.`)) progress.reset(code);
}
</script>
<header class="mb-10">
<h1 class="mb-3 text-4xl font-semibold tracking-tight sm:text-5xl">language learner</h1>
<p class="max-w-2xl text-lg ll-muted">
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.
</p>
</header>
{#if progress.loaded}
<section class="mb-10 grid gap-3 sm:grid-cols-3" aria-label="Today">
<div class="ll-card">
<p class="text-sm ll-muted">streak</p>
<p class="flex items-center gap-2 text-3xl font-semibold">
<Flame
size={26}
aria-hidden="true"
class={progress.streak() ? 'text-[var(--ll-gold)]' : 'll-muted'}
/>
{progress.streak()}
{progress.streak() === 1 ? 'day' : 'days'}
</p>
</div>
<div class="ll-card">
<p class="text-sm ll-muted">today</p>
<p class="mb-2 text-3xl font-semibold">
{progress.todayXp()}
<span class="text-base font-normal ll-muted">/ {progress.settings.goal} XP</span>
</p>
<ProgressBar
value={Math.min(progress.todayXp(), progress.settings.goal)}
max={progress.settings.goal}
label="Daily goal"
small
/>
</div>
<div class="ll-card">
<p class="text-sm ll-muted">total</p>
<p class="text-3xl font-semibold">
{Object.values(progress.data.langs).reduce((n, p) => n + (p?.xp ?? 0), 0)} XP
</p>
</div>
</section>
{/if}
<section aria-label="Languages" class="mb-14 grid gap-4 md:grid-cols-3">
{#each COURSES as c (c.code)}
{@const count = data.counts[c.code]}
<a
href={resolve('/projects/language-learner/[lang]', { lang: c.code })}
class="ll-card flex flex-col gap-3"
>
<div class="flex items-center justify-between">
<span class="ll-badge">{c.code.toUpperCase()}</span>
{#if progress.loaded && progress.streak(c.code)}
<span class="ll-pill"
><Flame size={14} aria-hidden="true" /> {progress.streak(c.code)}</span
>
{/if}
</div>
<div>
<h2 class="text-2xl font-semibold">{c.name}</h2>
<p class="ll-muted" lang={c.code}>{c.nativeName}</p>
</div>
<p class="text-sm ll-muted" lang={c.glossLang}>{c.tagline}</p>
<p class="text-sm ll-muted">
{count.lessons} lessons · {count.words} words · {count.levels[0]}–{count.levels.at(-1)}
</p>
<div class="mt-auto">
{#if progress.loaded}
<div class="mb-1 flex justify-between text-sm">
<span>{completed(c.code)} / {count.lessons} lessons</span>
{#if progress.due(c.code).length}
<span class="ll-muted">{progress.due(c.code).length} to review</span>
{/if}
</div>
<ProgressBar
value={completed(c.code)}
max={count.lessons}
label="{c.name} lessons done"
small
/>
{/if}
<span class="mt-4 inline-flex items-center gap-1 font-semibold"
>{completed(c.code) ? 'Continue' : 'Start'}
<ArrowRight size={16} aria-hidden="true" /></span
>
</div>
</a>
{/each}
</section>
<section class="mb-14 grid gap-8 md:grid-cols-2">
<div>
<h2 class="mb-4 text-2xl font-semibold">how it works</h2>
<ul class="space-y-3 ll-muted">
<li>
<strong class="text-[var(--site-text)]">Lessons</strong> explain one topic, then drill its words,
sentences and grammar. Wrong answers come back until you get them right.
</li>
<li>
<strong class="text-[var(--site-text)]">Reviews</strong> use spaced repetition: every word you
learn returns just before you'd forget it.
</li>
<li>
<strong class="text-[var(--site-text)]">Challenges</strong> are endless: a daily challenge, a
60-second sprint, survival with three lives, and generated drills for numbers, clock times, conjugation,
articles and particles.
</li>
<li>
<strong class="text-[var(--site-text)]">Start anywhere.</strong> Lessons are never locked, and
a level test lets you skip what you already know.
</li>
</ul>
</div>
<div>
<h2 class="mb-4 text-2xl font-semibold">settings</h2>
{#if progress.loaded}
<div class="ll-card space-y-4">
<label class="flex items-center justify-between gap-4">
<span
>Read words aloud <span class="block text-sm ll-muted">Uses your browser's voices</span
></span
>
<input
type="checkbox"
bind:checked={progress.settings.audio}
onchange={() => progress.save()}
/>
</label>
<label class="flex items-center justify-between gap-4">
<span>Show romanization for Korean</span>
<input
type="checkbox"
bind:checked={progress.settings.romanization}
onchange={() => progress.save()}
/>
</label>
{#each COURSES as c (c.code)}
<label class="flex items-center justify-between gap-4">
<span>Typing exercises in {c.name}</span>
<input
type="checkbox"
checked={progress.typing(c.code)}
onchange={(event) => {
progress.settings.typing[c.code] = event.currentTarget.checked;
progress.save();
}}
/>
</label>
{/each}
<div class="flex flex-wrap items-center justify-between gap-3">
<span>Daily goal</span>
<div class="flex gap-2">
{#each goals as goal (goal)}
<button
type="button"
class="ll-chip"
aria-pressed={progress.settings.goal === goal}
onclick={() => {
progress.settings.goal = goal;
progress.save();
}}>{goal} XP</button
>
{/each}
</div>
</div>
<div class="border-t border-[var(--site-border)] pt-4">
<p class="mb-3 text-sm ll-muted">
Progress is saved in this browser. Export it to move it to another device.
</p>
<div class="flex flex-wrap gap-2">
<button type="button" class="ll-button ll-button-quiet" onclick={exportProgress}
>Export</button
>
<button
type="button"
class="ll-button ll-button-quiet"
onclick={() => fileInput?.click()}>Import</button
>
<input
bind:this={fileInput}
type="file"
accept="application/json"
class="hidden"
onchange={importProgress}
/>
{#each COURSES as c (c.code)}
{#if progress.data.langs[c.code]}
<button
type="button"
class="ll-button ll-button-quiet"
onclick={() => reset(c.code, c.name)}>Reset {c.name}</button
>
{/if}
{/each}
</div>
{#if importError}<p class="mt-2 text-sm text-[var(--ll-wrong)]">{importError}</p>{/if}
</div>
</div>
{/if}
</div>
</section>

View File

@@ -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 };
};

View File

@@ -0,0 +1,211 @@
<script lang="ts">
import { resolve } from '$app/paths';
import ArrowLeft from 'lucide-svelte/icons/arrow-left';
import Check from 'lucide-svelte/icons/check';
import Flame from 'lucide-svelte/icons/flame';
import ProgressBar from '$lib/components/ProgressBar.svelte';
import { allLessons } from '$lib/language-learner/course';
import { progress } from '$lib/language-learner/progress.svelte';
import { dayKey } from '$lib/language-learner/srs';
import { MODES } from '$lib/language-learner/modes';
let { data } = $props();
const course = $derived(data.course);
const code = $derived(course.code);
const p = $derived(progress.loaded ? progress.data.langs[code] : undefined);
const lessons = $derived(allLessons(course));
const next = $derived(lessons.find((l) => !p?.lessons[l.id]));
const due = $derived(progress.loaded ? progress.due(code).length : 0);
const dailyScore = $derived(p?.daily[dayKey()]);
const challenges = $derived(MODES.filter((m) => m.group === 'challenge'));
const bestOf = (mode: string) => p?.bests[mode];
</script>
<a
href={resolve('/projects/language-learner')}
class="mb-6 inline-flex items-center gap-1 text-sm ll-muted hover:text-[var(--site-text)]"
><ArrowLeft size={14} aria-hidden="true" /> language learner</a
>
<header class="mb-8 flex flex-wrap items-end justify-between gap-4">
<div>
<h1 class="text-4xl font-semibold tracking-tight sm:text-5xl">{course.name}</h1>
<p class="mt-2 text-lg ll-muted">
<span lang={code}>{course.nativeName}</span> ·
<span lang={course.glossLang}>{course.tagline}</span>
</p>
</div>
{#if progress.loaded}
<div class="flex flex-wrap gap-2">
<span class="ll-pill"
><Flame size={14} aria-hidden="true" /> {progress.streak(code)} day streak</span
>
<span class="ll-pill">{progress.todayXp(code)} XP today</span>
<span class="ll-pill">{p?.xp ?? 0} XP total</span>
</div>
{/if}
</header>
<section class="mb-12 grid gap-4 md:grid-cols-2" aria-label="Up next">
<div class="ll-card flex flex-col justify-between gap-4">
{#if next}
<div>
<p class="text-sm ll-muted">
{p && Object.keys(p.lessons).length ? 'next lesson' : 'start here'}
</p>
<h2 class="text-2xl font-semibold" lang={code}>{next.title}</h2>
<p class="ll-muted">{next.summary}</p>
</div>
<a
href={resolve('/projects/language-learner/[lang]/lesson/[lesson]', {
lang: code,
lesson: next.id
})}
class="ll-button self-start">Start lesson</a
>
{:else}
<div>
<p class="text-sm ll-muted">course complete</p>
<h2 class="text-2xl font-semibold">Every lesson done</h2>
<p class="ll-muted">Keep your words fresh with reviews and challenges.</p>
</div>
{/if}
</div>
<div class="ll-card flex flex-col justify-between gap-4">
<div>
<p class="text-sm ll-muted">reviews</p>
<h2 class="text-2xl font-semibold">{due} due</h2>
<p class="ll-muted">
{#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}
</p>
</div>
<a
href={resolve('/projects/language-learner/[lang]/practice/[mode]', {
lang: code,
mode: 'review'
})}
class="ll-button self-start {due ? '' : 'll-button-quiet'}">Review</a
>
</div>
</section>
<section class="mb-12" aria-labelledby="challenges">
<div class="mb-4 flex flex-wrap items-end justify-between gap-3">
<h2 id="challenges" class="text-2xl font-semibold">challenges</h2>
<a
href={resolve('/projects/language-learner/[lang]/words', { lang: code })}
class="text-sm ll-muted underline underline-offset-2 hover:text-[var(--site-text)]"
>all words</a
>
</div>
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
{#each challenges as m (m.id)}
<a
href={resolve('/projects/language-learner/[lang]/practice/[mode]', {
lang: code,
mode: m.id
})}
class="ll-card"
>
<h3 class="font-semibold">{m.title}</h3>
<p class="text-sm ll-muted">{m.description}</p>
<p class="mt-2 text-sm">
{#if m.id === 'daily'}
{#if dailyScore !== undefined}<span class="ll-done">done today · {dailyScore}%</span
>{:else}not done yet{/if}
{:else if bestOf(m.id) !== undefined}
best: {bestOf(m.id)}
{/if}
</p>
</a>
{/each}
</div>
<h3 class="mt-8 mb-3 text-lg font-semibold">endless drills</h3>
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{#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)}
<a
href={resolve('/projects/language-learner/[lang]/practice/[mode]', {
lang: code,
mode: `drill-${d.id}`
})}
class="ll-card"
>
<h4 class="font-semibold">{d.title}</h4>
<p class="text-sm ll-muted">{d.description}</p>
{#if seen}<p class="mt-2 text-sm">
{seen} answered · {Math.round((right / seen) * 100)}% right
</p>{/if}
</a>
{/each}
</div>
</section>
<section aria-labelledby="levels">
<h2 id="levels" class="mb-4 text-2xl font-semibold">lessons</h2>
<div class="space-y-10">
{#each course.levels as lv (lv.id)}
{@const done = lv.lessons.filter((l) => p?.lessons[l.id]).length}
<div>
<div class="mb-3 flex flex-wrap items-end justify-between gap-3">
<div>
<h3 class="text-xl font-semibold">
<span class="ll-badge mr-2">{lv.band}</span>{lv.title}
</h3>
<p class="text-sm ll-muted">{lv.description}</p>
</div>
<div class="flex items-center gap-3">
<span class="text-sm ll-muted">{done}/{lv.lessons.length}</span>
<a
href={resolve('/projects/language-learner/[lang]/practice/[mode]', {
lang: code,
mode: `test-${lv.id}`
})}
class="ll-chip">{done === lv.lessons.length ? 'Level test' : 'Test out'}</a
>
</div>
</div>
<div class="mb-3">
<ProgressBar value={done} max={lv.lessons.length} label="{lv.band} progress" small />
</div>
<ol class="grid gap-2 md:grid-cols-2">
{#each lv.lessons as lesson, i (lesson.id)}
{@const record = p?.lessons[lesson.id]}
<li>
<a
href={resolve('/projects/language-learner/[lang]/lesson/[lesson]', {
lang: code,
lesson: lesson.id
})}
class="ll-card flex items-center gap-3 !p-3"
>
<span
class="ll-lesson-dot"
class:is-done={record}
class:is-next={next?.id === lesson.id}
>
{#if record}<Check size={16} aria-label="done" />{:else}{i + 1}{/if}
</span>
<span class="min-w-0 flex-1">
<span class="block truncate font-medium" lang={code}>{lesson.title}</span>
<span class="block truncate text-sm ll-muted">{lesson.summary}</span>
</span>
{#if record}<span class="text-sm ll-muted">{record.best}%</span>{/if}
</a>
</li>
{/each}
</ol>
</div>
{/each}
</div>
</section>

View File

@@ -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.`
}
};
};

View File

@@ -0,0 +1,173 @@
<script lang="ts">
import { resolve } from '$app/paths';
import { onMount } from 'svelte';
import ArrowLeft from 'lucide-svelte/icons/arrow-left';
import ArrowRight from 'lucide-svelte/icons/arrow-right';
import { lessonSession } from '$lib/language-learner/exercises';
import { romanize } from '$lib/language-learner/hangul';
import { progress } from '$lib/language-learner/progress.svelte';
import { seeded } from '$lib/language-learner/random';
import { hasVoice } from '$lib/language-learner/speech';
import type { Exercise, SessionResult } from '$lib/language-learner/types';
import Results from '$lib/language-learner/ui/Results.svelte';
import Session from '$lib/language-learner/ui/Session.svelte';
import Speak from '$lib/language-learner/ui/Speak.svelte';
let { data } = $props();
const course = $derived(data.course);
const lesson = $derived(data.lesson);
const code = $derived(course.code);
let voice = $state(false);
let queue = $state<Exercise[] | null>(null);
let result = $state<SessionResult | null>(null);
let run = $state(0);
const audio = $derived(voice && progress.settings.audio);
const record = $derived(
progress.loaded ? progress.data.langs[code]?.lessons[lesson.id] : undefined
);
const showRoman = $derived(code === 'ko' && progress.settings.romanization);
onMount(async () => {
voice = await hasVoice(course.voice);
});
// Reset when navigating between lessons.
$effect(() => {
void lesson.id;
queue = null;
result = null;
});
function start() {
queue = lessonSession(course, lesson, seeded(`${lesson.id}:${Date.now()}`), {
typing: progress.typing(code),
audio
});
result = null;
run++;
scrollTo({ top: 0 });
}
function finish(r: SessionResult) {
queue = null;
if (r.reason !== 'done') return;
const score = Math.round((r.firstTry / Math.max(1, r.total)) * 100);
const bonus = 10 + (score === 100 ? 5 : 0);
progress.completeLesson(code, lesson.id, score, [...lesson.words, ...lesson.sentences]);
progress.addXp(code, bonus);
result = { ...r, xp: r.xp + bonus };
scrollTo({ top: 0 });
}
</script>
{#if queue}
{#key run}
<Session {course} {queue} requeue {audio} onfinish={finish} />
{/key}
{:else}
<a
href={resolve('/projects/language-learner/[lang]', { lang: code })}
class="mb-6 inline-flex items-center gap-1 text-sm ll-muted hover:text-[var(--site-text)]"
><ArrowLeft size={14} aria-hidden="true" /> {course.name}</a
>
{#if result}
<Results
{result}
{code}
glossLang={course.glossLang}
title="Lesson complete"
subtitle={lesson.title}
score={Math.round((result.firstTry / Math.max(1, result.total)) * 100)}
percent
>
<button type="button" class="ll-button ll-button-quiet" onclick={start}>Practise again</button
>
{#if data.next}
<a
href={resolve('/projects/language-learner/[lang]/lesson/[lesson]', {
lang: code,
lesson: data.next.id
})}
class="ll-button">Next: {data.next.title} <ArrowRight size={16} aria-hidden="true" /></a
>
{:else}
<a href={resolve('/projects/language-learner/[lang]', { lang: code })} class="ll-button"
>Back to {course.name}</a
>
{/if}
</Results>
{/if}
<header class="mb-8 flex flex-wrap items-end justify-between gap-4">
<div>
<p class="mb-1 text-sm ll-muted">
<span class="ll-badge mr-2">{data.level.band}</span>{data.level.title}
</p>
<h1 class="text-3xl font-semibold tracking-tight sm:text-4xl" lang={code}>{lesson.title}</h1>
<p class="mt-1 text-lg ll-muted">{lesson.summary}</p>
</div>
<div class="flex items-center gap-3">
{#if record}<span class="text-sm ll-muted">best {record.best}% · {record.runs}×</span>{/if}
<button type="button" class="ll-button" onclick={start}
>{record ? 'Practise' : 'Start lesson'}</button
>
</div>
</header>
<div class="grid gap-10 lg:grid-cols-[minmax(0,3fr)_minmax(0,2fr)]">
<article class="ll-notes prose" lang={course.glossLang}>
<!-- eslint-disable-next-line svelte/no-at-html-tags -- trusted course content -->
{@html data.notesHtml}
</article>
<aside class="space-y-8">
<section>
<h2 class="mb-3 text-lg font-semibold">
{lesson.words.some((w) => w.kind === 'script') ? 'letters and words' : 'words'}
</h2>
<ul class="divide-y divide-[var(--site-border)] border-y border-[var(--site-border)]">
{#each lesson.words as w (w.id)}
<li class="flex items-center gap-3 py-2">
<span class="min-w-0 flex-1">
<span class="font-medium" lang={code}>{w.target.join(' / ')}</span>
{#if showRoman && w.kind === 'word'}<span class="ml-1 text-sm ll-muted"
>{romanize(w.target[0])}</span
>{/if}
<span
class="block text-sm ll-muted"
lang={w.kind === 'script' ? undefined : course.glossLang}
>{w.gloss.join(', ')}{w.note ? ` · ${w.note}` : ''}</span
>
</span>
{#if audio && (w.kind !== 'script' || /[가-힣]/.test(w.target[0]))}<Speak
text={w.target[0]}
voice={course.voice}
/>{/if}
</li>
{/each}
</ul>
</section>
{#if lesson.sentences.length}
<section>
<h2 class="mb-3 text-lg font-semibold">sentences</h2>
<ul class="divide-y divide-[var(--site-border)] border-y border-[var(--site-border)]">
{#each lesson.sentences as s (s.id)}
<li class="flex items-center gap-3 py-2">
<span class="min-w-0 flex-1">
<span lang={code}>{s.target[0]}</span>
<span class="block text-sm ll-muted" lang={course.glossLang}>{s.gloss[0]}</span>
</span>
{#if audio}<Speak text={s.target[0]} voice={course.voice} />{/if}
</li>
{/each}
</ul>
</section>
{/if}
<button type="button" class="ll-button w-full" onclick={start}
>{record ? 'Practise' : 'Start lesson'}</button
>
</aside>
</div>
{/if}

View File

@@ -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.`
}
};
};

View File

@@ -0,0 +1,346 @@
<script lang="ts">
import { resolve } from '$app/paths';
import { onMount } from 'svelte';
import ArrowLeft from 'lucide-svelte/icons/arrow-left';
import { mixed, reviews, scopeOf } from '$lib/language-learner/challenges';
import { progress } from '$lib/language-learner/progress.svelte';
import { seeded, type Rng } from '$lib/language-learner/random';
import { hasVoice } from '$lib/language-learner/speech';
import { dayKey } from '$lib/language-learner/srs';
import type { Exercise, SessionResult } from '$lib/language-learner/types';
import Results from '$lib/language-learner/ui/Results.svelte';
import Session from '$lib/language-learner/ui/Session.svelte';
let { data } = $props();
const course = $derived(data.course);
const code = $derived(course.code);
const mode = $derived(data.mode);
type Setup = {
queue: Exercise[];
more?: () => Exercise | null;
lives?: number;
seconds?: number;
requeue?: boolean;
xpPer?: number;
};
let voice = $state(false);
let setup = $state<Setup | null>(null);
let result = $state<SessionResult | null>(null);
type Headline = {
title: string;
score?: number;
percent?: boolean;
record: boolean;
note?: string;
};
let headline = $state<Headline | null>(null);
let run = $state(0);
let drillLevel = $state(1);
const audio = $derived(voice && progress.settings.audio);
const p = $derived(progress.loaded ? progress.data.langs[code] : undefined);
const scopeId = $derived(p?.scope ?? '');
const scope = $derived(scopeOf(course, p, scopeId));
const mixedMode = $derived(['daily', 'sprint', 'survival', 'endless'].includes(mode.kind));
const bestKey = $derived(mode.kind === 'drill' ? `drill-${mode.id}` : mode.kind);
const best = $derived(p?.bests[bestKey]);
const dailyDone = $derived(mode.kind === 'daily' ? p?.daily[dayKey()] : undefined);
const reviewInfo = $derived(
mode.kind === 'review' && p
? reviews(course, p, progress.due(code), seeded(0), { typing: false, audio: false })
: null
);
onMount(async () => {
voice = await hasVoice(course.voice);
});
// A sensible default drill level: the highest level you've started.
$effect(() => {
if (progress.loaded && mode.kind === 'drill')
drillLevel = scopeOf(course, progress.data.langs[code]).level;
});
$effect(() => {
void data.mode;
setup = null;
result = null;
});
function start() {
const opts = { typing: progress.typing(code), audio };
const rng: Rng = mode.kind === 'daily' ? seeded(`${dayKey()}:${code}`) : seeded(Date.now());
const lang = progress.lang(code);
const drills = data.drills;
switch (mode.kind) {
case 'daily': {
const next = mixed({ course, scope, drills, rng, opts, p: lang });
setup = { queue: Array.from({ length: 20 }, next), xpPer: 3 };
break;
}
case 'sprint':
setup = {
queue: [],
more: mixed({ course, scope, drills, rng, opts, p: lang }),
seconds: 60,
xpPer: 1
};
break;
case 'survival':
setup = {
queue: [],
more: mixed({ course, scope, drills, rng, opts, p: lang, ramp: true }),
lives: 3
};
break;
case 'endless':
setup = { queue: [], more: mixed({ course, scope, drills, rng, opts, p: lang }) };
break;
case 'review': {
const r = reviews(course, lang, progress.due(code), rng, opts);
setup = { queue: r.queue, more: r.more, requeue: true };
break;
}
case 'drill': {
const drill = drills.find((d) => d.id === mode.id)!;
const level = drillLevel;
setup = { queue: [], more: () => drill.make({ rng, level, opts, course }) };
break;
}
case 'test': {
const lv = course.levels.find((l) => l.id === mode.level)!;
const next = mixed({
course,
scope: {
lessons: lv.lessons,
level: scopeOf(course, undefined, lv.id).level,
label: lv.band
},
drills: [],
rng,
opts
});
setup = { queue: Array.from({ length: 20 }, next), xpPer: 2 };
break;
}
}
result = null;
headline = null;
run++;
}
function finish(r: SessionResult) {
const s = setup;
setup = null;
if (!s) return;
const accuracy = Math.round((r.correct / Math.max(1, r.answered)) * 100);
switch (mode.kind) {
case 'daily': {
if (r.answered < 20) {
headline = {
title: 'Daily challenge stopped',
record: false,
note: 'Finish all 20 to record your score.'
};
break;
}
const first = p?.daily[dayKey()] === undefined;
progress.recordDaily(code, accuracy);
if (first) {
progress.addXp(code, 20);
r = { ...r, xp: r.xp + 20 };
}
headline = {
title: 'Daily challenge done',
score: accuracy,
percent: true,
record: false,
note: first
? 'Your score for today is saved. +20 XP bonus.'
: 'Only your first run of the day counts.'
};
break;
}
case 'sprint':
case 'survival':
case 'endless':
case 'drill':
headline = {
title:
mode.kind === 'survival'
? 'Out of lives'
: mode.kind === 'sprint'
? "Time's up"
: 'Session over',
score: r.correct,
record: r.correct > 0 && progress.recordBest(code, bestKey, r.correct)
};
break;
case 'review':
headline = { title: 'Review done', record: false };
break;
case 'test': {
const passed = r.answered >= 20 && accuracy >= 80;
const lv = course.levels.find((l) => l.id === mode.level)!;
if (passed)
for (const lesson of lv.lessons)
progress.completeLesson(code, lesson.id, accuracy, [
...lesson.words,
...lesson.sentences
]);
headline = {
title: passed ? `${lv.band} passed` : r.answered < 20 ? 'Test stopped' : 'Not yet',
score: accuracy,
percent: true,
record: false,
note: passed
? `Every ${lv.band} lesson is now marked as done and its words are in your reviews.`
: 'You need 80% on all 20 questions. The lessons will get you there.'
};
break;
}
}
result = r;
}
</script>
{#if setup}
{#key run}
<Session
{course}
queue={setup.queue}
more={setup.more}
lives={setup.lives}
seconds={setup.seconds}
requeue={setup.requeue}
xpPer={setup.xpPer}
{audio}
onfinish={finish}
/>
{/key}
{:else}
<a
href={resolve('/projects/language-learner/[lang]', { lang: code })}
class="mb-6 inline-flex items-center gap-1 text-sm ll-muted hover:text-[var(--site-text)]"
><ArrowLeft size={14} aria-hidden="true" /> {course.name}</a
>
{#if result && headline}
<Results
{result}
{code}
glossLang={course.glossLang}
title={headline.title}
subtitle={mode.title}
score={headline.score}
percent={headline.percent}
record={headline.record}
>
{#if headline.note}<p class="mb-2 w-full ll-muted">{headline.note}</p>{/if}
<button type="button" class="ll-button" onclick={start}>Again</button>
<a
href={resolve('/projects/language-learner/[lang]', { lang: code })}
class="ll-button ll-button-quiet">Back to {course.name}</a
>
</Results>
{/if}
<header class="mb-6">
<h1 class="text-3xl font-semibold tracking-tight sm:text-4xl">{mode.title}</h1>
<p class="mt-1 text-lg ll-muted">{mode.description}</p>
</header>
{#if progress.loaded}
<div class="ll-card max-w-2xl space-y-5">
{#if mixedMode}
<div>
<p class="mb-2 font-semibold">Questions from</p>
<div class="flex flex-wrap gap-2">
<button
type="button"
class="ll-chip"
aria-pressed={scopeId === ''}
onclick={() => {
progress.lang(code).scope = '';
progress.save();
}}>What I have learned</button
>
{#each course.levels as lv (lv.id)}
<button
type="button"
class="ll-chip"
aria-pressed={scopeId === lv.id}
onclick={() => {
progress.lang(code).scope = lv.id;
progress.save();
}}>{lv.band}</button
>
{/each}
<button
type="button"
class="ll-chip"
aria-pressed={scopeId === 'all'}
onclick={() => {
progress.lang(code).scope = 'all';
progress.save();
}}>Everything</button
>
</div>
<p class="mt-2 text-sm ll-muted">
{scope.lessons.length} lessons: {scope.lessons
.slice(0, 4)
.map((l) => l.title)
.join(', ')}{scope.lessons.length > 4 ? ', …' : ''}
</p>
</div>
{/if}
{#if mode.kind === 'drill'}
<div>
<p class="mb-2 font-semibold">Difficulty</p>
<div class="flex flex-wrap gap-2">
{#each [1, 2, 3, 4, 5] as n (n)}
<button
type="button"
class="ll-chip"
aria-pressed={drillLevel === n}
onclick={() => (drillLevel = n)}>{['A1', 'A2', 'B1', 'B2', 'C1'][n - 1]}</button
>
{/each}
</div>
</div>
{/if}
{#if mode.kind === 'review' && reviewInfo}
<p>
{#if !reviewInfo.known}
No words to review yet. Finish a lesson first.
{:else}
<strong>{reviewInfo.due}</strong> 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}
</p>
{/if}
<div class="flex flex-wrap items-center gap-4">
<button
type="button"
class="ll-button"
disabled={mode.kind === 'review' && !reviewInfo?.known}
onclick={start}>Start</button
>
{#if dailyDone !== undefined}
<span class="ll-done">Done today: {dailyDone}%</span>
{:else if best !== undefined && mode.kind !== 'daily'}
<span class="ll-muted">Best: {best}</span>
{/if}
{#if mode.kind === 'survival' || mode.kind === 'endless' || mode.kind === 'drill'}
<span class="text-sm ll-muted">Endless: stop any time with ✕.</span>
{/if}
</div>
</div>
{/if}
{/if}

View File

@@ -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 }
};
};

View File

@@ -0,0 +1,139 @@
<script lang="ts">
import { resolve } from '$app/paths';
import { onMount } from 'svelte';
import ArrowLeft from 'lucide-svelte/icons/arrow-left';
import { allLessons } from '$lib/language-learner/course';
import { romanize } from '$lib/language-learner/hangul';
import { progress } from '$lib/language-learner/progress.svelte';
import { hasVoice } from '$lib/language-learner/speech';
import { mastery, type Mastery } from '$lib/language-learner/srs';
import { normalize } from '$lib/language-learner/text';
import Speak from '$lib/language-learner/ui/Speak.svelte';
let { data } = $props();
const course = $derived(data.course);
const code = $derived(course.code);
type Filter = 'all' | Mastery | 'sentences';
let filter = $state<Filter>('all');
let query = $state('');
let voice = $state(false);
onMount(async () => {
voice = await hasVoice(course.voice);
});
const lessonTitle = $derived(new Map(allLessons(course).map((l) => [l.id, l.title])));
// A word taught in several lessons is listed once, under the lesson that introduces it.
const rows = $derived(
course.levels
.flatMap((lv) =>
lv.lessons.flatMap((l) =>
[...l.words, ...l.sentences].map((entry) => ({ entry, band: lv.band }))
)
)
.filter((r, i, all) => all.findIndex((x) => x.entry.id === r.entry.id) === i)
.map((r) => ({
...r,
mastery: mastery(progress.loaded ? progress.data.langs[code]?.cards[r.entry.id] : undefined)
}))
);
const counts = $derived(
rows.reduce(
(acc, r) => {
if (r.entry.kind !== 'sentence') acc[r.mastery]++;
return acc;
},
{ new: 0, learning: 0, familiar: 0, known: 0, mastered: 0 } as Record<Mastery, number>
)
);
const visible = $derived.by(() => {
const q = normalize(query);
return rows.filter(
(r) =>
(filter === 'all'
? r.entry.kind !== 'sentence'
: filter === 'sentences'
? r.entry.kind === 'sentence'
: r.entry.kind !== 'sentence' && r.mastery === filter) &&
(!q || [...r.entry.target, ...r.entry.gloss].some((t) => normalize(t).includes(q)))
);
});
const filters: { id: Filter; label: string }[] = [
{ id: 'all', label: 'All words' },
{ id: 'new', label: 'New' },
{ id: 'learning', label: 'Learning' },
{ id: 'familiar', label: 'Familiar' },
{ id: 'known', label: 'Known' },
{ id: 'mastered', label: 'Mastered' },
{ id: 'sentences', label: 'Sentences' }
];
</script>
<a
href={resolve('/projects/language-learner/[lang]', { lang: code })}
class="mb-6 inline-flex items-center gap-1 text-sm ll-muted hover:text-[var(--site-text)]"
><ArrowLeft size={14} aria-hidden="true" /> {course.name}</a
>
<header class="mb-6">
<h1 class="text-3xl font-semibold tracking-tight sm:text-4xl">{course.name} words</h1>
<p class="mt-1 ll-muted">
{#each ['learning', 'familiar', 'known', 'mastered'] as const as m (m)}
<span class="mr-4 inline-flex items-center gap-1.5"
><span class="ll-mastery" data-level={m}></span>{counts[m]} {m}</span
>
{/each}
<span class="inline-flex items-center gap-1.5"
><span class="ll-mastery"></span>{counts.new} new</span
>
</p>
</header>
<div class="mb-4 flex flex-wrap items-center gap-2">
<input
class="ll-input max-w-xs"
type="search"
placeholder="Search"
aria-label="Search words"
bind:value={query}
/>
{#each filters as f (f.id)}
<button
type="button"
class="ll-chip"
aria-pressed={filter === f.id}
onclick={() => (filter = f.id)}>{f.label}</button
>
{/each}
</div>
<p class="mb-2 text-sm ll-muted">{visible.length} shown</p>
<ul class="divide-y divide-[var(--site-border)] border-y border-[var(--site-border)]">
{#each visible.slice(0, 400) as r (r.entry.id)}
<li class="flex items-center gap-3 py-2">
<span class="ll-mastery" data-level={r.mastery} title={r.mastery}></span>
<span class="min-w-0 flex-1">
<span class="font-medium" lang={code}>{r.entry.target.join(' / ')}</span>
{#if code === 'ko' && progress.settings.romanization && r.entry.kind !== 'script'}
<span class="ml-1 text-sm ll-muted">{romanize(r.entry.target[0])}</span>
{/if}
<span
class="block text-sm ll-muted"
lang={r.entry.kind === 'script' ? undefined : course.glossLang}
>{r.entry.gloss.join(', ')}</span
>
</span>
<span class="hidden text-right text-xs ll-muted sm:block"
>{r.band} · {lessonTitle.get(r.entry.lesson)}</span
>
{#if voice && progress.settings.audio}<Speak
text={r.entry.target[0]}
voice={course.voice}
/>{/if}
</li>
{/each}
</ul>
{#if visible.length > 400}<p class="mt-2 text-sm ll-muted">
Showing the first 400. Search to narrow it down.
</p>{/if}

View File

@@ -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.`
}
};
};

View File

@@ -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);
}

View File

@@ -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 {