Holy Sh*t Dutch

2026 · Backend in production; mobile applications in testing

A Dutch vocabulary application for iOS, iPadOS and Android, with translations in English and Traditional Chinese. Review intervals are scheduled with FSRS-6; all content is served by a Go backend.

  • FSRS-6 scheduling
  • Native iOS and Android clients
  • Passkey authentication
Role
iOS and Android applications, backend, content pipeline
Stack
  • SwiftUI
  • Kotlin
  • Jetpack Compose
  • Go
  • Echo v5
  • PostgreSQL 18
  • sqlc
  • OpenAPI
  • Astro
  • Svelte
  • React
  • FreeBSD
Five screens of the Android application in light appearance. Shown are the home screen with deck progress, a flashcard, a multiple-choice listening exercise, a typing and sentence-building exercise, and the lesson summary. All values are fictitious.

Overview

The application teaches Dutch vocabulary and phrases in short lessons. Translations are provided in English and Traditional Chinese (Taiwan forms); the public name of the application in Chinese is 荷理學 (Hélǐxué). The project comprises four repositories: a SwiftUI client, a Kotlin client with feature parity, a Go backend and a static landing page.

The catalogue contains 14 categories, 58 decks and 2,689 cards. Each card carries a CEFR level, an IPA transcription and, for most entries, an example sentence.

Lesson design

A lesson combines up to ten cards due for review, ordered by overdueness, with up to five new cards from the selected deck. New cards are introduced with pronunciation, an example sentence and, for concrete nouns, a picture. Exercises include meaning selection, word selection, listening, typed answers, sentence construction from tiles and pair matching.

Learners do not rate their own recall. Each answer is graded by the application as correct, almost correct or wrong, and the grade is mapped to the FSRS ratings Good, Hard and Again respectively.

Scheduling

Review intervals are computed with FSRS-6. The algorithm was ported from py-fsrs to Swift and to Kotlin, and both ports are tested against fixtures generated by py-fsrs, which keeps the two clients numerically consistent. For the Android port, domain rules first implemented in Swift, such as answer grading and lesson construction, were written up as specifications before reimplementation.

Content pipeline

The clients ship no content. Cards, audio and pictures are served by the backend and cached on the device, so a correction reaches all users without an application update.

  • Cards are maintained as TSV seed files and compiled into a catalogue by a Python build step. Card identifiers are UUIDv7 and never change, since on-device review history is keyed by them.
  • IPA transcriptions are generated offline with espeak-ng and corrected by hand where necessary.
  • Audio is synthesised with three Azure neural voices for every word and example sentence, approximately 15,600 clips in total. Each clip is stored on a CDN under an immutable URL, so cached files never require revalidation.
  • Pictures are proposed from stock-photo and Wikimedia Commons sources and approved in an admin panel (React, shadcn/ui), which records author, licence and source for each image.

Clients

The iOS client targets iOS 16 so that older iPads remain supported; this excludes the Observation framework and SwiftData. The Android client targets Android 13 and later. Decks can be downloaded for offline use, and progress is synchronised through a state-based protocol shared by both platforms, so one account can be used on either. The Android user interface is covered by Roborazzi screenshot tests, from which the images on this page are taken.

Backend and authentication

The backend is a single static Go binary (Echo v5, pgx, sqlc, goose) implementing an OpenAPI 3 contract, on PostgreSQL 18, deployed behind Caddy on FreeBSD. Accounts consist of a username and a passkey; no password or e-mail address is collected. Usernames are encrypted at rest and looked up by an HMAC of the normalised name. End-to-end encryption of review progress, with keys derived through the WebAuthn PRF extension, is under consideration; it would require raising the iOS minimum to version 18.