Skip to content

Commit daef0f6

Browse files
Yifeng "Evan" Wangclaude
andcommitted
feat(course): full Korean grammar course, foundations, glossary & vocab
- 26 bilingual (zh/en) chapters: elementary e01–e12, intermediate i01–i08, advanced a01–a06 — 303 examples, every snippet type-checks against the DSL and resolves exactly to its Korean sentence (verify-snippets ✓) - Foundations article rewritten for Korean (SOV, particles, 받침 allomorphs, 해요체 vs 합니다체, type-level conjugation) - 109 vocab entries (Revised Romanization) covering every course word (verify-vocab ✓); reverse index links 303 examples to the glossary - DSL fix: widen CopulaPart's 받침 allomorph in the PhrasePart union - harness: verify-snippets module-detection + unescape resolved quotes; resolve.ts unescapes quotes for in-app display - build robustness: gen-reverse-index tolerates an empty vocab/entries dir Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 5fd1c0b commit daef0f6

37 files changed

Lines changed: 14930 additions & 429 deletions

playground/scripts/gen-reverse-index.mjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@ const { extractWords } = evalModule(path.join(SRC, "vocab/extract.ts"));
4040
// (particles, です…) aren't extracted, so they're expected to have no links.
4141
const contentWords = new Set();
4242
const entriesDir = path.join(SRC, "vocab/entries");
43-
for (const f of fs.readdirSync(entriesDir).filter((f) => f.endsWith(".ts"))) {
43+
const entryFiles = fs.existsSync(entriesDir)
44+
? fs.readdirSync(entriesDir).filter((f) => f.endsWith(".ts"))
45+
: [];
46+
for (const f of entryFiles) {
4447
const mod = evalModule(path.join(entriesDir, f));
4548
for (const e of mod.default || mod.entries || []) if (e?.word) contentWords.add(e.word);
4649
}

playground/scripts/verify-snippets.mjs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ const options = {
9191
target: ts.ScriptTarget.ES2020,
9292
module: ts.ModuleKind.ESNext,
9393
moduleResolution: ts.ModuleResolutionKind.Bundler,
94+
// Treat every snippet file as its own module so same-named aliases (e.g. a
95+
// final `type S`) in different snippets don't collide in a shared global
96+
// scope. A snippet that genuinely forgets its `import` will still fail with
97+
// "Cannot find name …", which is what we want to catch.
98+
moduleDetection: ts.ModuleDetectionKind.Force,
9499
strict: true,
95100
skipLibCheck: true,
96101
noEmit: true,
@@ -134,7 +139,10 @@ function resolvedString(ex) {
134139
const type = checker.getTypeFromTypeNode(last.type);
135140
const str = checker.typeToString(type, undefined, ts.TypeFormatFlags.NoTruncation);
136141
const m = str.match(/^"([\s\S]*)"$/);
137-
return m ? m[1] : str;
142+
// typeToString renders a string-literal type with the inner " and \ escaped;
143+
// unescape them to recover the real sentence (so quotes inside a sentence,
144+
// e.g. quotation with 라고, compare equal to example.jp).
145+
return m ? m[1].replace(/\\(["\\])/g, "$1") : str;
138146
} catch {
139147
return null;
140148
}

playground/src/analysis/resolve.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ function extractStringLiteral(quickInfo: string): string | null {
2525
if (eq < 0) return null;
2626
const rhs = quickInfo.slice(eq + 1).trim();
2727
const m = rhs.match(/^"([\s\S]*)"$/);
28-
return m ? m[1]! : null;
28+
// Unescape the \" and \\ that quick-info adds when rendering a string-literal
29+
// type, so a sentence containing quotes (e.g. 라고 quotation) shows correctly.
30+
return m ? m[1]!.replace(/\\(["\\])/g, "$1") : null;
2931
}
3032

3133
/**

playground/src/concepts/articles/architecture.ts

Lines changed: 464 additions & 415 deletions
Large diffs are not rendered by default.

playground/src/tutorial/chapters/a01.ts

Lines changed: 362 additions & 0 deletions
Large diffs are not rendered by default.

playground/src/tutorial/chapters/a02.ts

Lines changed: 321 additions & 0 deletions
Large diffs are not rendered by default.

playground/src/tutorial/chapters/a03.ts

Lines changed: 366 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
import type { Chapter } from "../types";
2+
3+
const chapter: Chapter = {
4+
id: "a04",
5+
level: "advanced",
6+
order: 4,
7+
titleEn: "Commands and propositives in context",
8+
titleZh: "命令与提议的实际运用",
9+
summaryEn:
10+
"Korean has two major command forms and one propositive. The **plain imperative** (-아/어라) is used in casual or intimate speech — talking down to children, writing instructions, or addressing close friends in 반말. The **honorific imperative** (-세요, from -으시어요) is polite and is the default command form in everyday conversation. The **propositive** (-(으)ㄹ까요?) invites the listener to do something together: *Shall we …?*\n\nAll three forms appear frequently in dialogue. This chapter drills each form independently, then shows how speakers shift register — using 반말 vs 존댓말 — depending on who they are talking to.",
11+
summaryZh:
12+
"韩语有两种主要命令形式和一种提议形式。**반말命令句**(-아/어라)用于随意或亲密的场合——对孩子说话、书写指令,或在反语(반말)中与亲近的朋友交流。**尊称命令句**(-세요,源自-으시어요)礼貌得体,是日常对话中默认的命令形式。**提议句**(-(으)ㄹ까요?)邀请对方一起做某事:*我们……好吗?*\n\n这三种形式在对话中频繁出现。本章逐一练习每种形式,然后展示说话者如何根据交谈对象切换语域——使用반말还是존댓말。",
13+
points: [
14+
{
15+
id: "a04-1",
16+
titleEn: "Plain imperative -아/어라 (반말 commands)",
17+
titleZh: "반말命令句 -아/어라",
18+
bodyEn:
19+
"The **plain imperative** is formed by appending **-라** to the verb's informal stem (`inf` field in the DSL). Because the `inf` stem already ends in **아** (after positive-harmony consonant-final stems) or **어** (elsewhere), the full ending is always **-아라** or **-어라**.\n\nThis form is restricted to **반말** — it is used between close friends of equal standing, from adults to young children, or in written instructions/signs. Using 아/어라 toward a stranger or superior is rude. In the `Imperative` form the DSL simply appends **라** to the `inf` value:\n\n- `먹다` inf `먹어` → `먹어라`\n- `앉다` inf `앉아` → `앉아라`\n- `읽다` inf `읽어` → `읽어라`",
20+
bodyZh:
21+
"**반말命令句**通过在动词的非正式词干(DSL 中的 `inf` 字段)后附加 **-라** 构成。由于 `inf` 词干已以 **아**(阳性和谐辅音结尾词干)或 **어**(其他情形)结尾,完整结尾始终为 **-아라** 或 **-어라**。\n\n该形式仅限于 **반말** 语域——用于地位平等的亲密朋友之间、成人对幼童,或书面指令/标牌。对陌生人或上级使用아/어라属于失礼行为。DSL 中的 `Imperative` 形式直接在 `inf` 值后附加 **라**:\n\n- `먹다` inf `먹어` → `먹어라`\n- `앉다` inf `앉아` → `앉아라`\n- `읽다` inf `읽어` → `읽어라`",
22+
examples: [
23+
{
24+
jp: "여기 앉아라.",
25+
reading: "yeogi anjara",
26+
en: "Sit down here.",
27+
zh: "坐在这里。",
28+
code: `import type { Verb, Adverb, VerbPart, AdverbPart, PunctuationPart, Sentence } from "typed-korean";
29+
30+
31+
// 앉다: regular consonant-final verb (sit)
32+
type 앉다 = Verb & { dict:"앉다"; stem:"앉"; inf:"앉아"; past:"앉았"; eu:"앉으"; prospective:"앉을"; formalStem:"앉습니" };
33+
34+
type S = Sentence<[
35+
AdverbPart<Adverb<"여기">>,
36+
VerbPart<앉다, "Imperative">,
37+
PunctuationPart<".">
38+
]>;
39+
// S = "여기 앉아라."`,
40+
},
41+
{
42+
jp: "빨리 먹어라.",
43+
reading: "ppalli meogeora",
44+
en: "Eat quickly.",
45+
zh: "快点吃。",
46+
code: `import type { Verb, Adverb, VerbPart, AdverbPart, PunctuationPart, Sentence } from "typed-korean";
47+
48+
49+
type 먹다 = Verb & { dict:"먹다"; stem:"먹"; inf:"먹어"; past:"먹었"; eu:"먹으"; prospective:"먹을"; formalStem:"먹습니" };
50+
51+
type S = Sentence<[
52+
AdverbPart<Adverb<"빨리">>,
53+
VerbPart<먹다, "Imperative">,
54+
PunctuationPart<".">
55+
]>;
56+
// S = "빨리 먹어라."`,
57+
},
58+
{
59+
jp: "책을 읽어라.",
60+
reading: "chaegeul ilgeora",
61+
en: "Read the book.",
62+
zh: "读书。",
63+
code: `import type { CommonNoun, Verb, VerbPart, NounPart, ParticlePart, PunctuationPart, Sentence } from "typed-korean";
64+
65+
66+
type 읽다 = Verb & { dict:"읽다"; stem:"읽"; inf:"읽어"; past:"읽었"; eu:"읽으"; prospective:"읽을"; formalStem:"읽습니" };
67+
68+
type S = Sentence<[
69+
NounPart<CommonNoun<"책">>,
70+
ParticlePart<"을">,
71+
VerbPart<읽다, "Imperative">,
72+
PunctuationPart<".">
73+
]>;
74+
// S = "책을 읽어라."`,
75+
},
76+
],
77+
},
78+
{
79+
id: "a04-2",
80+
titleEn: "Honorific imperative -세요 (polite requests)",
81+
titleZh: "尊称命令句 -세요(礼貌请求)",
82+
bodyEn:
83+
"The **honorific imperative** is formed by appending **-세요** to the verb's **으-stem** (`eu` field). For vowel-final stems the 으 is dropped (오 → 오세요), while consonant-final stems insert 으 (앉으 → 앉으세요). This form belongs to **해요체** and is the polite, default command in everyday conversation.\n\n합니다체 has no direct parallel for commands; 하십시오 (from -으십시오) is a very formal written/broadcast register that sounds stiff in normal speech. In practice, -세요 covers both casual-polite and formal-polite contexts:\n\n- `오다` eu `오` → `오세요`\n- `앉다` eu `앉으` → `앉으세요`\n- `기다리다` eu `기다리` → `기다리세요`",
84+
bodyZh:
85+
"**尊称命令句**通过在动词的 **으-词干**(`eu` 字段)后附加 **-세요** 构成。元音结尾词干省略으(오 → 오세요),辅音结尾词干则插入으(앉으 → 앉으세요)。该形式属于 **해요체**,是日常对话中礼貌的默认命令形式。\n\n합니다체 没有与之直接对应的命令形式;하십시오(源自-으십시오)是非常正式的书面/广播语域,在普通对话中听起来生硬。实际上,-세요 可覆盖随意礼貌和正式礼貌两种语境:\n\n- `오다` eu `오` → `오세요`\n- `앉다` eu `앉으` → `앉으세요`\n- `기다리다` eu `기다리` → `기다리세요`",
86+
examples: [
87+
{
88+
jp: "빨리 오세요.",
89+
reading: "ppalli oseyo",
90+
en: "Please come quickly.",
91+
zh: "请快点来。",
92+
code: `import type { Verb, Adverb, VerbPart, AdverbPart, PunctuationPart, Sentence } from "typed-korean";
93+
94+
95+
type 오다 = Verb & { dict:"오다"; stem:"오"; inf:"와"; past:"왔"; eu:"오"; prospective:"올"; formalStem:"옵니" };
96+
97+
type S = Sentence<[
98+
AdverbPart<Adverb<"빨리">>,
99+
VerbPart<오다, "Honorific">,
100+
PunctuationPart<".">
101+
]>;
102+
// S = "빨리 오세요."`,
103+
},
104+
{
105+
jp: "여기 앉으세요.",
106+
reading: "yeogi anjeuseyo",
107+
en: "Please sit here.",
108+
zh: "请坐在这里。",
109+
code: `import type { Verb, Adverb, VerbPart, AdverbPart, PunctuationPart, Sentence } from "typed-korean";
110+
111+
112+
// 앉다: eu stem "앉으" → 앉으세요
113+
type 앉다 = Verb & { dict:"앉다"; stem:"앉"; inf:"앉아"; past:"앉았"; eu:"앉으"; prospective:"앉을"; formalStem:"앉습니" };
114+
115+
type S = Sentence<[
116+
AdverbPart<Adverb<"여기">>,
117+
VerbPart<앉다, "Honorific">,
118+
PunctuationPart<".">
119+
]>;
120+
// S = "여기 앉으세요."`,
121+
},
122+
{
123+
jp: "잠깐 기다리세요.",
124+
reading: "jamkkan gidariseyo",
125+
en: "Please wait a moment.",
126+
zh: "请稍等一下。",
127+
code: `import type { Verb, Adverb, VerbPart, AdverbPart, PunctuationPart, Sentence } from "typed-korean";
128+
129+
130+
type 기다리다 = Verb & { dict:"기다리다"; stem:"기다리"; inf:"기다려"; past:"기다렸"; eu:"기다리"; prospective:"기다릴"; formalStem:"기다립니" };
131+
132+
type S = Sentence<[
133+
AdverbPart<Adverb<"잠깐">>,
134+
VerbPart<기다리다, "Honorific">,
135+
PunctuationPart<".">
136+
]>;
137+
// S = "잠깐 기다리세요."`,
138+
},
139+
],
140+
},
141+
{
142+
id: "a04-3",
143+
titleEn: "Propositive -(으)ㄹ까요? (shall we…?)",
144+
titleZh: "提议句 -(으)ㄹ까요?(我们……好吗?)",
145+
bodyEn:
146+
"The **propositive** attaches **까요** to the verb's `prospective` stem. Since the prospective stem already encodes whether 을 or ㄹ is needed (vowel-final stems carry ㄹ, consonant-final stems carry 을), the DSL `Propositive` form always produces the correct surface:\n\n- Vowel-final / ㄹ-final: `공부하다` prospective `공부할` → **공부할까요**\n- Vowel-final: `기다리다` prospective `기다릴` → **기다릴까요**\n- Consonant-final: `먹다` prospective `먹을` → **먹을까요**\n\nThe propositive is **해요체** and is the same form across all polite contexts. It always ends with a **?** in writing. Adding **우리** (we) or **같이** (together) makes the suggestion more explicit.",
147+
bodyZh:
148+
"**提议句**将 **까요** 附加到动词的 `prospective` 词干上。由于展望词干已编码了是否需要 을 或 ㄹ(元音结尾词干带 ㄹ,辅音结尾词干带 을),DSL 的 `Propositive` 形式始终生成正确的表层形式:\n\n- 元音结尾 / ㄹ-结尾:`공부하다` 展望形 `공부할` → **공부할까요**\n- 元音结尾:`기다리다` 展望形 `기다릴` → **기다릴까요**\n- 辅音结尾:`먹다` 展望形 `먹을` → **먹을까요**\n\n提议句属于 **해요체**,在所有礼貌语境中形式相同。书写时句末始终加 **?**。添加 **우리**(我们)或 **같이**(一起)使建议更加明确。",
149+
examples: [
150+
{
151+
jp: "같이 공부할까요?",
152+
reading: "gachi gongbuhalkkayo",
153+
en: "Shall we study together?",
154+
zh: "我们一起学习好吗?",
155+
code: `import type { HadaVerb, Adverb, VerbPart, AdverbPart, PunctuationPart, Sentence } from "typed-korean";
156+
157+
158+
type 공부하다 = HadaVerb & { dict:"공부하다"; stem:"공부하"; inf:"공부해"; past:"공부했"; eu:"공부하"; prospective:"공부할"; formalStem:"공부합니" };
159+
160+
type S = Sentence<[
161+
AdverbPart<Adverb<"같이">>,
162+
VerbPart<공부하다, "Propositive">,
163+
PunctuationPart<"?">
164+
]>;
165+
// S = "같이 공부할까요?"`,
166+
},
167+
{
168+
jp: "여기에서 기다릴까요?",
169+
reading: "yeogieseo gidarilkkayo",
170+
en: "Shall we wait here?",
171+
zh: "我们在这里等好吗?",
172+
code: `import type { Pronoun, Verb, VerbPart, PronounPart, ParticlePart, PunctuationPart, Sentence } from "typed-korean";
173+
174+
175+
type 기다리다 = Verb & { dict:"기다리다"; stem:"기다리"; inf:"기다려"; past:"기다렸"; eu:"기다리"; prospective:"기다릴"; formalStem:"기다립니" };
176+
177+
type S = Sentence<[
178+
PronounPart<Pronoun<"여기">>,
179+
ParticlePart<"에서">,
180+
VerbPart<기다리다, "Propositive">,
181+
PunctuationPart<"?">
182+
]>;
183+
// S = "여기에서 기다릴까요?"`,
184+
},
185+
{
186+
jp: "우리 같이 밥을 먹을까요?",
187+
reading: "uri gachi babeul meogeulkkayo",
188+
en: "Shall we eat together?",
189+
zh: "我们一起吃饭好吗?",
190+
code: `import type { CommonNoun, Pronoun, Verb, Adverb, VerbPart, NounPart, PronounPart, AdverbPart, ParticlePart, PunctuationPart, Sentence } from "typed-korean";
191+
192+
193+
type 먹다 = Verb & { dict:"먹다"; stem:"먹"; inf:"먹어"; past:"먹었"; eu:"먹으"; prospective:"먹을"; formalStem:"먹습니" };
194+
195+
type S = Sentence<[
196+
PronounPart<Pronoun<"우리">>,
197+
AdverbPart<Adverb<"같이">>,
198+
NounPart<CommonNoun<"밥">>,
199+
ParticlePart<"을">,
200+
VerbPart<먹다, "Propositive">,
201+
PunctuationPart<"?">
202+
]>;
203+
// S = "우리 같이 밥을 먹을까요?"`,
204+
},
205+
],
206+
},
207+
{
208+
id: "a04-4",
209+
titleEn: "Register contrast in dialogue",
210+
titleZh: "对话中的语域对比",
211+
bodyEn:
212+
"In real dialogue, speakers choose among plain imperative, honorific imperative, and propositive based on relationship and intent. Compare the same core meaning across registers:\n\n| Intent | 반말 (plain) | 해요체 (polite) |\n|---|---|---|\n| command to be quiet | 조용히 해라 | 조용히 하세요 |\n| propositive to start | — | 같이 시작할까요? |\n\nThe propositive does not have a 반말 equivalent in everyday speech (there are archaic/literary forms, but they are not used). Notably, **어서 오세요!** (Welcome! / Please come in!) is a fixed honorific expression that every speaker encounters constantly — it combines the adverb 어서 (hurry, quickly) with the honorific imperative of 오다.",
213+
bodyZh:
214+
"在真实对话中,说话者根据关系和意图在반말命令句、尊称命令句和提议句之间切换。比较不同语域中表达同一核心含义的方式:\n\n| 意图 | 반말(随意) | 해요체(礼貌) |\n|---|---|---|\n| 命令安静 | 조용히 해라 | 조용히 하세요 |\n| 提议开始 | — | 같이 시작할까요? |\n\n提议句在日常口语中没有对应的반말形式(存在古语/文学形式,但已不使用)。值得注意的是,**어서 오세요!**(欢迎!/ 请进!)是一个固定的尊称表达,每个说话者都会频繁遇到——它由副词어서(快,赶快)与오다的尊称命令形式组合而成。",
215+
examples: [
216+
{
217+
jp: "조용히 해라.",
218+
reading: "joyonghi haera",
219+
en: "Be quiet. (casual)",
220+
zh: "安静。(随意语气)",
221+
code: `import type { HadaVerb, Adverb, VerbPart, AdverbPart, PunctuationPart, Sentence } from "typed-korean";
222+
223+
224+
type 하다 = HadaVerb & { dict:"하다"; stem:"하"; inf:"해"; past:"했"; eu:"하"; prospective:"할"; formalStem:"합니" };
225+
226+
type S = Sentence<[
227+
AdverbPart<Adverb<"조용히">>,
228+
VerbPart<하다, "Imperative">,
229+
PunctuationPart<".">
230+
]>;
231+
// S = "조용히 해라."`,
232+
},
233+
{
234+
jp: "조용히 하세요.",
235+
reading: "joyonghi haseyo",
236+
en: "Please be quiet. (polite)",
237+
zh: "请安静。(礼貌语气)",
238+
code: `import type { HadaVerb, Adverb, VerbPart, AdverbPart, PunctuationPart, Sentence } from "typed-korean";
239+
240+
241+
type 하다 = HadaVerb & { dict:"하다"; stem:"하"; inf:"해"; past:"했"; eu:"하"; prospective:"할"; formalStem:"합니" };
242+
243+
type S = Sentence<[
244+
AdverbPart<Adverb<"조용히">>,
245+
VerbPart<하다, "Honorific">,
246+
PunctuationPart<".">
247+
]>;
248+
// S = "조용히 하세요."`,
249+
},
250+
{
251+
jp: "같이 시작할까요?",
252+
reading: "gachi sijakhalkkayo",
253+
en: "Shall we start together?",
254+
zh: "我们一起开始好吗?",
255+
code: `import type { HadaVerb, Adverb, VerbPart, AdverbPart, PunctuationPart, Sentence } from "typed-korean";
256+
257+
258+
type 시작하다 = HadaVerb & { dict:"시작하다"; stem:"시작하"; inf:"시작해"; past:"시작했"; eu:"시작하"; prospective:"시작할"; formalStem:"시작합니" };
259+
260+
type S = Sentence<[
261+
AdverbPart<Adverb<"같이">>,
262+
VerbPart<시작하다, "Propositive">,
263+
PunctuationPart<"?">
264+
]>;
265+
// S = "같이 시작할까요?"`,
266+
},
267+
],
268+
},
269+
],
270+
};
271+
272+
export default chapter;

0 commit comments

Comments
 (0)