lingo-free.openapps.xyz

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.
Owned app · rev 8af89d2cc9f8
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.
← All files
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
8af89d2cc9 · 17 hours ago
Bind agent address in policy
0018350275 · 19 hours ago
Use OpenApps service URLs and add contribution strip
805365daf4 · 19 hours ago
Bounded source sync
3f2fb3b265 · 2 days ago
Bounded source sync
7dcb580324 · 2 days ago
Bounded source sync
dcd05c0944 · 2 days ago