Anglicize tippen's codebase and finish pending UI/curriculum cleanup
- Rename all German identifiers, types, mode ids, file names, CSS classes and data-attributes to English throughout tippen/src; only user-facing text (lesson titles, word lists, labels, spoken praise) stays German. - Add a word/nonsense-word list to the "Übung: die Grundstellung" home-row lesson in the curriculum. - Remove the unused HandHint component and speech.ts, and carry forward the in-progress App.tsx/component/generator/progress edits from other sessions. - Refresh the regenerated music-library cache index. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,17 +1,17 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { FIRST_LESSON_ID, LESSONS, WELTEN, lessonById, lessonsOfWelt, nextLesson } from "../curriculum";
|
||||
import { FIRST_LESSON_ID, LESSONS, WORLDS, lessonById, lessonsOfWorld, nextLesson } from "../curriculum";
|
||||
import { fingerOf, keyForChar, needsShift } from "../fingers";
|
||||
|
||||
describe("LESSONS", () => {
|
||||
it("has unique ids and consecutive numbers", () => {
|
||||
const ids = LESSONS.map((lesson) => lesson.id);
|
||||
expect(new Set(ids).size).toBe(ids.length);
|
||||
LESSONS.forEach((lesson, i) => expect(lesson.nummer).toBe(i + 1));
|
||||
LESSONS.forEach((lesson, i) => expect(lesson.number).toBe(i + 1));
|
||||
});
|
||||
|
||||
it("starts on the two keys with the tactile bumps", () => {
|
||||
expect(LESSONS[0]!.neueKeys).toEqual(["f", "j"]);
|
||||
expect(LESSONS[0]!.newKeys).toEqual(["f", "j"]);
|
||||
expect(FIRST_LESSON_ID).toBe(LESSONS[0]!.id);
|
||||
});
|
||||
|
||||
@@ -19,50 +19,50 @@ describe("LESSONS", () => {
|
||||
// The first draft opened with all four left-hand keys at once, which is a steep
|
||||
// first five minutes for a six-year-old. Two at a time, always.
|
||||
for (const lesson of LESSONS) {
|
||||
expect(lesson.neueKeys.length, `${lesson.nummer}: ${lesson.titel}`).toBeLessThanOrEqual(2);
|
||||
expect(lesson.newKeys.length, `${lesson.number}: ${lesson.title}`).toBeLessThanOrEqual(2);
|
||||
}
|
||||
});
|
||||
|
||||
it("teaches Welt 1 as mirrored finger pairs, one finger per hand", () => {
|
||||
const paare = lessonsOfWelt(1).filter((lesson) => lesson.neueKeys.length === 2);
|
||||
expect(paare.length).toBe(4);
|
||||
for (const lesson of paare) {
|
||||
const [links, rechts] = lesson.neueKeys.map((key) => fingerOf(key)!);
|
||||
expect(links!.hand).toBe("links");
|
||||
expect(rechts!.hand).toBe("rechts");
|
||||
it("teaches world 1 as mirrored finger pairs, one finger per hand", () => {
|
||||
const pairs = lessonsOfWorld(1).filter((lesson) => lesson.newKeys.length === 2);
|
||||
expect(pairs.length).toBe(4);
|
||||
for (const lesson of pairs) {
|
||||
const [left, right] = lesson.newKeys.map((key) => fingerOf(key)!);
|
||||
expect(left!.hand).toBe("left");
|
||||
expect(right!.hand).toBe("right");
|
||||
// Same finger on each hand - "die Zeigefinger", "die Mittelfinger", …
|
||||
expect(links!.id.replace("links-", "")).toBe(rechts!.id.replace("rechts-", ""));
|
||||
expect(left!.id.replace("left-", "")).toBe(right!.id.replace("right-", ""));
|
||||
}
|
||||
});
|
||||
|
||||
it("has the whole Grundstellung active by the end of Welt 1", () => {
|
||||
const letzte = lessonsOfWelt(1).at(-1)!;
|
||||
for (const key of "asdfjklö") expect(letzte.activeKeys).toContain(key);
|
||||
expect(letzte.activeKeys).toContain(" ");
|
||||
it("has the whole home row active by the end of world 1", () => {
|
||||
const last = lessonsOfWorld(1).at(-1)!;
|
||||
for (const key of "asdfjklö") expect(last.activeKeys).toContain(key);
|
||||
expect(last.activeKeys).toContain(" ");
|
||||
});
|
||||
|
||||
it("makes every round a real block of practice, not a handful of keys", () => {
|
||||
// The first version ran 12 characters in Welt 1 - long enough to finish, too short
|
||||
// The first version ran 12 characters in world 1 - long enough to finish, too short
|
||||
// for a rhythm to form or for the speed score to mean anything.
|
||||
for (const lesson of LESSONS) {
|
||||
if (lesson.art === "saetze") {
|
||||
expect(lesson.chunks, `${lesson.nummer}: ${lesson.titel}`).toBeGreaterThanOrEqual(10);
|
||||
} else if (lesson.art === "woerter") {
|
||||
expect(lesson.chunks, `${lesson.nummer}: ${lesson.titel}`).toBeGreaterThanOrEqual(25);
|
||||
if (lesson.kind === "sentences") {
|
||||
expect(lesson.chunks, `${lesson.number}: ${lesson.title}`).toBeGreaterThanOrEqual(10);
|
||||
} else if (lesson.kind === "words") {
|
||||
expect(lesson.chunks, `${lesson.number}: ${lesson.title}`).toBeGreaterThanOrEqual(25);
|
||||
} else {
|
||||
expect(lesson.chunks * lesson.chunkSize, `${lesson.nummer}: ${lesson.titel}`).toBeGreaterThanOrEqual(60);
|
||||
expect(lesson.chunks * lesson.chunkSize, `${lesson.number}: ${lesson.title}`).toBeGreaterThanOrEqual(60);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("still starts gentler than it ends", () => {
|
||||
const laenge = (lesson: (typeof LESSONS)[number]) => lesson.chunks * lesson.chunkSize;
|
||||
expect(laenge(lessonsOfWelt(3)[0]!)).toBeGreaterThan(laenge(lessonsOfWelt(1)[0]!));
|
||||
const length = (lesson: (typeof LESSONS)[number]) => lesson.chunks * lesson.chunkSize;
|
||||
expect(length(lessonsOfWorld(3)[0]!)).toBeGreaterThan(length(lessonsOfWorld(1)[0]!));
|
||||
});
|
||||
|
||||
it("has more sentences to draw from than a round uses, so a round never has to repeat", () => {
|
||||
for (const lesson of LESSONS.filter((l) => l.art === "saetze")) {
|
||||
expect(lesson.woerter.length, `${lesson.nummer}: ${lesson.titel}`).toBeGreaterThanOrEqual(lesson.chunks);
|
||||
for (const lesson of LESSONS.filter((l) => l.kind === "sentences")) {
|
||||
expect(lesson.words.length, `${lesson.number}: ${lesson.title}`).toBeGreaterThanOrEqual(lesson.chunks);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -76,7 +76,7 @@ describe("LESSONS", () => {
|
||||
|
||||
it("introduces every new key into its own activeKeys", () => {
|
||||
for (const lesson of LESSONS) {
|
||||
for (const key of lesson.neueKeys) {
|
||||
for (const key of lesson.newKeys) {
|
||||
if (key === "⇧") continue; // Shift is not a character the generator can emit.
|
||||
expect(lesson.activeKeys).toContain(key);
|
||||
}
|
||||
@@ -99,27 +99,27 @@ describe("LESSONS", () => {
|
||||
// so a lesson can type them as soon as it has the ß or a key.
|
||||
for (const lesson of LESSONS) {
|
||||
const active = new Set(lesson.activeKeys);
|
||||
for (const wort of lesson.woerter) {
|
||||
for (const char of wort) {
|
||||
for (const word of lesson.words) {
|
||||
for (const char of word) {
|
||||
expect(
|
||||
active.has(keyForChar(char)),
|
||||
`Lektion ${lesson.nummer} (${lesson.titel}): "${wort}" braucht "${char}" (Taste ${keyForChar(char)})`,
|
||||
`Lektion ${lesson.number} (${lesson.title}): "${word}" braucht "${char}" (Taste ${keyForChar(char)})`,
|
||||
).toBe(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("only uses shifted characters once the Umschalttaste is taught", () => {
|
||||
const shiftAb = LESSONS.find((lesson) => lesson.neueKeys.includes("⇧"))!.nummer;
|
||||
it("only uses shifted characters once shift is taught", () => {
|
||||
const shiftFrom = LESSONS.find((lesson) => lesson.newKeys.includes("⇧"))!.number;
|
||||
for (const lesson of LESSONS) {
|
||||
for (const wort of lesson.woerter) {
|
||||
for (const char of wort) {
|
||||
for (const word of lesson.words) {
|
||||
for (const char of word) {
|
||||
if (!needsShift(char)) continue;
|
||||
expect(
|
||||
lesson.nummer,
|
||||
`Lektion ${lesson.nummer}: "${wort}" braucht Umschalt für "${char}"`,
|
||||
).toBeGreaterThanOrEqual(shiftAb);
|
||||
lesson.number,
|
||||
`Lektion ${lesson.number}: "${word}" braucht Umschalt für "${char}"`,
|
||||
).toBeGreaterThanOrEqual(shiftFrom);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,59 +127,59 @@ describe("LESSONS", () => {
|
||||
|
||||
it("only offers word modes where words exist", () => {
|
||||
for (const lesson of LESSONS) {
|
||||
const wortModi = lesson.modi.some((modus) => modus === "fuettern" || modus === "rennen");
|
||||
expect(wortModi).toBe(lesson.woerter.length > 0);
|
||||
const wordModes = lesson.modes.some((mode) => mode === "feed" || mode === "race");
|
||||
expect(wordModes).toBe(lesson.words.length > 0);
|
||||
}
|
||||
});
|
||||
|
||||
it("always offers the pressure-free Perlentaucher", () => {
|
||||
for (const lesson of LESSONS) expect(lesson.modi).toContain("perlen");
|
||||
it("always offers the pressure-free pearl-diving mode", () => {
|
||||
for (const lesson of LESSONS) expect(lesson.modes).toContain("pearls");
|
||||
});
|
||||
|
||||
it("teaches capitals only once the Umschalttaste exists", () => {
|
||||
it("teaches capitals only once shift exists", () => {
|
||||
for (const lesson of LESSONS) {
|
||||
const hatGross = lesson.woerter.some((wort) => wort !== wort.toLowerCase());
|
||||
if (hatGross) expect(lesson.welt).toBeGreaterThanOrEqual(4);
|
||||
const hasUppercase = lesson.words.some((word) => word !== word.toLowerCase());
|
||||
if (hasUppercase) expect(lesson.world).toBeGreaterThanOrEqual(4);
|
||||
}
|
||||
});
|
||||
|
||||
it("gives every new key a lesson of its own after Welt 1", () => {
|
||||
// One key at a time is the pacing decision: Welt 1 pairs a finger across both
|
||||
it("gives every new key a lesson of its own after world 1", () => {
|
||||
// One key at a time is the pacing decision: world 1 pairs a finger across both
|
||||
// hands, everything after introduces exactly one key or none.
|
||||
for (const lesson of LESSONS) {
|
||||
if (lesson.welt === 1) continue;
|
||||
expect(lesson.neueKeys.length, `${lesson.nummer}: ${lesson.titel}`).toBeLessThanOrEqual(2);
|
||||
if (lesson.world === 1) continue;
|
||||
expect(lesson.newKeys.length, `${lesson.number}: ${lesson.title}`).toBeLessThanOrEqual(2);
|
||||
}
|
||||
});
|
||||
|
||||
it("follows every pair of new keys with an Übung", () => {
|
||||
const uebungen = LESSONS.filter((lesson) => lesson.istUebung);
|
||||
expect(uebungen.length).toBeGreaterThanOrEqual(12);
|
||||
// An Übung never introduces anything, and always has something to practise.
|
||||
for (const lesson of uebungen) {
|
||||
expect(lesson.neueKeys).toEqual([]);
|
||||
it("follows every pair of new keys with a drill", () => {
|
||||
const drills = LESSONS.filter((lesson) => lesson.isDrill);
|
||||
expect(drills.length).toBeGreaterThanOrEqual(12);
|
||||
// A drill never introduces anything, and always has something to practise.
|
||||
for (const lesson of drills) {
|
||||
expect(lesson.newKeys).toEqual([]);
|
||||
expect(lesson.activeKeys.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("is long enough to be a real course", () => {
|
||||
expect(LESSONS.length).toBeGreaterThanOrEqual(40);
|
||||
expect(WELTEN.length).toBe(5);
|
||||
expect(WORLDS.length).toBe(5);
|
||||
});
|
||||
|
||||
it("never leaves a Welt without a run of at least three lessons", () => {
|
||||
for (const welt of WELTEN) expect(lessonsOfWelt(welt.nummer).length).toBeGreaterThanOrEqual(3);
|
||||
it("never leaves a world without a run of at least three lessons", () => {
|
||||
for (const world of WORLDS) expect(lessonsOfWorld(world.number).length).toBeGreaterThanOrEqual(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Welten", () => {
|
||||
it("has lessons in every Welt", () => {
|
||||
for (const welt of WELTEN) expect(lessonsOfWelt(welt.nummer).length).toBeGreaterThan(0);
|
||||
describe("Worlds", () => {
|
||||
it("has lessons in every world", () => {
|
||||
for (const world of WORLDS) expect(lessonsOfWorld(world.number).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("gives every Welt its own aquarium creature", () => {
|
||||
const belohnungen = WELTEN.map((welt) => welt.belohnung);
|
||||
expect(new Set(belohnungen).size).toBe(belohnungen.length);
|
||||
it("gives every world its own aquarium creature", () => {
|
||||
const rewards = WORLDS.map((world) => world.reward);
|
||||
expect(new Set(rewards).size).toBe(rewards.length);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -193,7 +193,7 @@ describe("navigation", () => {
|
||||
});
|
||||
|
||||
it("looks lessons up by id", () => {
|
||||
expect(lessonById(FIRST_LESSON_ID)?.nummer).toBe(1);
|
||||
expect(lessonById(FIRST_LESSON_ID)?.number).toBe(1);
|
||||
expect(lessonById("gibt-es-nicht")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user