Source
This app’s code and revision history are public. Download a copy or explore the files below.
Public
The app’s code and revision history are public.
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone 7efc97d0dde0b2a3e729ccf9
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
src/lib/courses.ts
// Loads course files on demand (each is its own chunk, cached by the service worker) and indexes them:
// every word and sentence by key, the lessons in order, and word hints for tapping a word.
import { useEffect, useState } from "react";
import type { Course, LangId, Lesson, Sentence, Unit, Word } from "../../functions/lib/course";
import { fold } from "./answer";
const files = import.meta.glob<{ default: Course }>("../../functions/courses/*.json");
const loading = new Map<LangId, Promise<Course>>();
export function loadCourse(id: LangId): Promise<Course> {
let p = loading.get(id);
if (!p) {
const file = files[`../../functions/courses/${id}.json`];
p = file ? file().then((m) => m.default) : Promise.reject(new Error("No such course."));
p.catch(() => loading.delete(id));
loading.set(id, p);
}
return p;
}
export function useCourse(id: LangId | null): Course | null | "error" {
const [course, setCourse] = useState<Course | null | "error">(null);
useEffect(() => {
if (!id) return;
let live = true;
setCourse(null);
loadCourse(id).then((c) => live && setCourse(c), () => live && setCourse("error"));
return () => { live = false; };
}, [id]);
return course;
}
export interface Item { key: string; u: number; l: number; word?: Word; sentence?: Sentence }
export interface Step { u: number; l: number; unit: Unit; lesson: Lesson; key: string }
export interface Index {
items: Map<string, Item>;
steps: Step[]; // every lesson in course order
hints: Map<string, [string, string][]>; // folded token -> [word, English]
proper: Set<string>; // tokens that keep their capital letter anywhere in a sentence (names, German nouns, "I")
}
export const wordKey = (w: Word) => `w:${w.t}`;
export const sentenceKey = (s: Sentence) => `s:${s.t}`;
export const lessonKey = (u: number, l: number) => `${u}.${l}`;
/** Words of a sentence as tiles: split on spaces, outer punctuation removed. */
export function tiles(s: string): string[] {
return s.split(/[\s ]+/).map((w) => w.replace(/^[¿¡"“«(]+|[?!.,;:"”»)…]+$/g, "")).filter(Boolean);
}
const built = new WeakMap<Course, Index>();
export function indexOf(course: Course): Index {
const cached = built.get(course);
if (cached) return cached;
const items = new Map<string, Item>();
const steps: Step[] = [];
const hints = new Map<string, [string, string][]>();
const proper = new Set<string>(["I"]);
for (const unit of course.units) {
for (const lesson of unit.lessons) {
steps.push({ u: unit.n, l: lesson.n, unit, lesson, key: lessonKey(unit.n, lesson.n) });
for (const word of lesson.words) {
items.set(wordKey(word), { key: wordKey(word), u: unit.n, l: lesson.n, word });
for (const t of tiles(word.t)) {
const k = fold(t.toLowerCase());
const list = hints.get(k) || [];
if (!list.some(([w]) => w === word.t)) list.push([word.t, word.e]);
hints.set(k, list);
}
}
for (const sentence of lesson.sentences) {
items.set(sentenceKey(sentence), { key: sentenceKey(sentence), u: unit.n, l: lesson.n, sentence });
for (const text of [sentence.t, sentence.e]) tiles(text).slice(1).forEach((t) => { if (/^\p{Lu}/u.test(t)) proper.add(t); });
}
}
}
const index = { items, steps, hints, proper };
built.set(course, index);
return index;
}
Change history
Point OpenApps links to openapps.free
Bind agent address in policy
Use OpenApps service URLs and add contribution strip
Bounded source sync
Bounded source sync
Bounded source sync