add language learner project
Some checks failed
Test and deploy staging / staging (push) Has been cancelled
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:
26
README.md
Executable file → Normal file
26
README.md
Executable file → Normal 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
2
eslint.config.js
Executable file → Normal file
@@ -20,7 +20,7 @@ export default [
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['**/*.svelte'],
|
||||
files: ['**/*.svelte', '**/*.svelte.ts'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
parser: ts.parser
|
||||
|
||||
33
src/lib/language-learner/challenges.test.ts
Normal file
33
src/lib/language-learner/challenges.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
128
src/lib/language-learner/challenges.ts
Normal file
128
src/lib/language-learner/challenges.ts
Normal 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 };
|
||||
}
|
||||
85
src/lib/language-learner/content.test.ts
Normal file
85
src/lib/language-learner/content.test.ts
Normal 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 }));
|
||||
});
|
||||
});
|
||||
111
src/lib/language-learner/course.ts
Normal file
111
src/lib/language-learner/course.ts
Normal 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);
|
||||
}
|
||||
567
src/lib/language-learner/courses/en/a1.ts
Normal file
567
src/lib/language-learner/courses/en/a1.ts
Normal 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
|
||||
`
|
||||
})
|
||||
]
|
||||
);
|
||||
419
src/lib/language-learner/courses/en/a2.ts
Normal file
419
src/lib/language-learner/courses/en/a2.ts
Normal 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
|
||||
`
|
||||
})
|
||||
]
|
||||
);
|
||||
401
src/lib/language-learner/courses/en/b1.ts
Normal file
401
src/lib/language-learner/courses/en/b1.ts
Normal 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
|
||||
`
|
||||
})
|
||||
]
|
||||
);
|
||||
347
src/lib/language-learner/courses/en/b2.ts
Normal file
347
src/lib/language-learner/courses/en/b2.ts
Normal 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
|
||||
`
|
||||
})
|
||||
]
|
||||
);
|
||||
318
src/lib/language-learner/courses/en/c1.ts
Normal file
318
src/lib/language-learner/courses/en/c1.ts
Normal 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
|
||||
`
|
||||
})
|
||||
]
|
||||
);
|
||||
18
src/lib/language-learner/courses/en/index.ts
Normal file
18
src/lib/language-learner/courses/en/index.ts
Normal 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]
|
||||
);
|
||||
48
src/lib/language-learner/courses/index.ts
Normal file
48
src/lib/language-learner/courses/index.ts
Normal 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;
|
||||
}
|
||||
465
src/lib/language-learner/courses/ko/a1.ts
Normal file
465
src/lib/language-learner/courses/ko/a1.ts
Normal 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: 버스___ 타요. | 를 | 에; 로; 가
|
||||
`
|
||||
})
|
||||
]
|
||||
);
|
||||
426
src/lib/language-learner/courses/ko/a2.ts
Normal file
426
src/lib/language-learner/courses/ko/a2.ts
Normal 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.
|
||||
`
|
||||
})
|
||||
]
|
||||
);
|
||||
404
src/lib/language-learner/courses/ko/b1.ts
Normal file
404
src/lib/language-learner/courses/ko/b1.ts
Normal 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: 화가 ___. | 났어요 | 했어요; 나요; 있었어요
|
||||
`
|
||||
})
|
||||
]
|
||||
);
|
||||
355
src/lib/language-learner/courses/ko/b2.ts
Normal file
355
src/lib/language-learner/courses/ko/b2.ts
Normal 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 ___ | 나무에서 | 산에서; 하늘에서; 집에서
|
||||
`
|
||||
})
|
||||
]
|
||||
);
|
||||
297
src/lib/language-learner/courses/ko/c1.ts
Normal file
297
src/lib/language-learner/courses/ko/c1.ts
Normal 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): | 십중팔구 | 일석이조; 유비무환; 대기만성
|
||||
`
|
||||
})
|
||||
]
|
||||
);
|
||||
334
src/lib/language-learner/courses/ko/hangul.ts
Normal file
334
src/lib/language-learner/courses/ko/hangul.ts
Normal 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
|
||||
`
|
||||
})
|
||||
]
|
||||
);
|
||||
19
src/lib/language-learner/courses/ko/index.ts
Normal file
19
src/lib/language-learner/courses/ko/index.ts
Normal 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]
|
||||
);
|
||||
558
src/lib/language-learner/courses/nl/a1.ts
Normal file
558
src/lib/language-learner/courses/nl/a1.ts
Normal 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.
|
||||
`
|
||||
})
|
||||
]
|
||||
);
|
||||
503
src/lib/language-learner/courses/nl/a2.ts
Normal file
503
src/lib/language-learner/courses/nl/a2.ts
Normal 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
|
||||
`
|
||||
})
|
||||
]
|
||||
);
|
||||
411
src/lib/language-learner/courses/nl/b1.ts
Normal file
411
src/lib/language-learner/courses/nl/b1.ts
Normal 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
|
||||
`
|
||||
})
|
||||
]
|
||||
);
|
||||
370
src/lib/language-learner/courses/nl/b2.ts
Normal file
370
src/lib/language-learner/courses/nl/b2.ts
Normal 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
|
||||
`
|
||||
})
|
||||
]
|
||||
);
|
||||
318
src/lib/language-learner/courses/nl/c1.ts
Normal file
318
src/lib/language-learner/courses/nl/c1.ts
Normal 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
|
||||
`
|
||||
})
|
||||
]
|
||||
);
|
||||
18
src/lib/language-learner/courses/nl/index.ts
Normal file
18
src/lib/language-learner/courses/nl/index.ts
Normal 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]
|
||||
);
|
||||
455
src/lib/language-learner/drills/en.ts
Normal file
455
src/lib/language-learner/drills/en.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
14
src/lib/language-learner/drills/index.ts
Normal file
14
src/lib/language-learner/drills/index.ts
Normal 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;
|
||||
}
|
||||
382
src/lib/language-learner/drills/ko.ts
Normal file
382
src/lib/language-learner/drills/ko.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
592
src/lib/language-learner/drills/nl.ts
Normal file
592
src/lib/language-learner/drills/nl.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
67
src/lib/language-learner/drills/shared.ts
Normal file
67
src/lib/language-learner/drills/shared.ts
Normal 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))
|
||||
];
|
||||
319
src/lib/language-learner/exercises.ts
Normal file
319
src/lib/language-learner/exercises.ts
Normal 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]));
|
||||
}
|
||||
40
src/lib/language-learner/hangul.test.ts
Normal file
40
src/lib/language-learner/hangul.test.ts
Normal 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('의사예요');
|
||||
});
|
||||
});
|
||||
217
src/lib/language-learner/hangul.ts
Normal file
217
src/lib/language-learner/hangul.ts
Normal 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];
|
||||
}
|
||||
77
src/lib/language-learner/korean-verbs.test.ts
Normal file
77
src/lib/language-learner/korean-verbs.test.ts
Normal 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(/[가-힣]/);
|
||||
});
|
||||
});
|
||||
343
src/lib/language-learner/korean-verbs.ts
Normal file
343
src/lib/language-learner/korean-verbs.ts
Normal 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
|
||||
`
|
||||
)
|
||||
];
|
||||
41
src/lib/language-learner/modes.ts
Normal file
41
src/lib/language-learner/modes.ts
Normal 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'
|
||||
}
|
||||
];
|
||||
37
src/lib/language-learner/numbers.test.ts
Normal file
37
src/lib/language-learner/numbers.test.ts
Normal 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('네');
|
||||
});
|
||||
});
|
||||
166
src/lib/language-learner/numbers.ts
Normal file
166
src/lib/language-learner/numbers.ts
Normal 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];
|
||||
}
|
||||
194
src/lib/language-learner/progress.svelte.ts
Normal file
194
src/lib/language-learner/progress.svelte.ts
Normal 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();
|
||||
12
src/lib/language-learner/random.test.ts
Normal file
12
src/lib/language-learner/random.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
50
src/lib/language-learner/random.ts
Normal file
50
src/lib/language-learner/random.ts
Normal 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;
|
||||
}
|
||||
43
src/lib/language-learner/speech.ts
Normal file
43
src/lib/language-learner/speech.ts
Normal 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);
|
||||
}
|
||||
28
src/lib/language-learner/srs.test.ts
Normal file
28
src/lib/language-learner/srs.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
76
src/lib/language-learner/srs.ts
Normal file
76
src/lib/language-learner/srs.ts
Normal 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);
|
||||
67
src/lib/language-learner/text.ts
Normal file
67
src/lib/language-learner/text.ts
Normal 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, ' ');
|
||||
114
src/lib/language-learner/types.ts
Normal file
114
src/lib/language-learner/types.ts
Normal 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';
|
||||
};
|
||||
294
src/lib/language-learner/ui/Exercise.svelte
Normal file
294
src/lib/language-learner/ui/Exercise.svelte
Normal 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>
|
||||
79
src/lib/language-learner/ui/Results.svelte
Normal file
79
src/lib/language-learner/ui/Results.svelte
Normal 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>
|
||||
246
src/lib/language-learner/ui/Session.svelte
Normal file
246
src/lib/language-learner/ui/Session.svelte
Normal 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>
|
||||
22
src/lib/language-learner/ui/Speak.svelte
Normal file
22
src/lib/language-learner/ui/Speak.svelte
Normal 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
|
||||
>
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 },
|
||||
|
||||
13
src/routes/projects/language-learner/+layout.svelte
Normal file
13
src/routes/projects/language-learner/+layout.svelte
Normal 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>
|
||||
20
src/routes/projects/language-learner/+page.server.ts
Normal file
20
src/routes/projects/language-learner/+page.server.ts
Normal 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[] }>
|
||||
};
|
||||
};
|
||||
245
src/routes/projects/language-learner/+page.svelte
Normal file
245
src/routes/projects/language-learner/+page.svelte
Normal 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>
|
||||
10
src/routes/projects/language-learner/[lang]/+layout.ts
Normal file
10
src/routes/projects/language-learner/[lang]/+layout.ts
Normal 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 };
|
||||
};
|
||||
211
src/routes/projects/language-learner/[lang]/+page.svelte
Normal file
211
src/routes/projects/language-learner/[lang]/+page.svelte
Normal 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>
|
||||
11
src/routes/projects/language-learner/[lang]/+page.ts
Normal file
11
src/routes/projects/language-learner/[lang]/+page.ts
Normal 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.`
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -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}
|
||||
@@ -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.`
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -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}
|
||||
@@ -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 }
|
||||
};
|
||||
};
|
||||
139
src/routes/projects/language-learner/[lang]/words/+page.svelte
Normal file
139
src/routes/projects/language-learner/[lang]/words/+page.svelte
Normal 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}
|
||||
11
src/routes/projects/language-learner/[lang]/words/+page.ts
Normal file
11
src/routes/projects/language-learner/[lang]/words/+page.ts
Normal 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.`
|
||||
}
|
||||
};
|
||||
};
|
||||
450
src/routes/projects/language-learner/language-learner.css
Normal file
450
src/routes/projects/language-learner/language-learner.css
Normal 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);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user