Skip to content

Commit 2b7758e

Browse files
Strong's for the German corpus + German word study + the strongs-de pipeline (#24)
* Menu rationalization: a bar of roles, Settings folds its depths away The bar becomes the five hats a reader wears — Read · Study · Preach · Share · Sing — and the tools move one layer down (maintainer direction, 2026-08-09): - Study is the hub (internally still the Explore screen): Memorize joins its cards, lights the Study tab, and its hub's back goes up one layer. - Preach raises Present; Sing is the hymnal; labels via new nav.* keys. - Share is a real destination in both shells (ShareScreen.svelte / ui/ShareScreen.kt): the QR + link card, plus the church fields moved out of Settings to sit beside the QR their setting feeds, with a visit button (the recipient's path). The header share icon and ShareAppDialog are gone. - Settings: everyday rows stay (language, theme, type, sliders, verse-per-line); everything else folds behind ONE Advanced disclosure in both shells. Church and Welcome left the dialog entirely. - The ≡/⋮ utilities are reachable from EVERY destination (session-level menu + ScreenBar slot on web; UtilityMenu on every Android bar), ending the 3-tap Read-tab detour to Settings both shells shared. Welcome shows for every reader there (falls back to the new-believer welcome) — down from four surfaces to one. - The Android weave-map card renders explore.weaveMap like the web, instead of the drifted map.chordMap. e2e updated across nine specs (labels, the Study→Memorize path, the Advanced expansions, the firstrun welcome consolidation, dialog-focus re-anchored on BookNav); FEATURE-MANIFEST's menu section rewritten. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * Strong's tags for the German corpus + the strongs-de pipeline - data/luther1912.jsonl gains 344,330 Strong's tags (364,489 codes) merged from the Zefania XML "Luther 1912 mit Strongs" (toledot.info; the file's header declares the text public domain — BIBLIOGRAPHY.md records it). Tokens unchanged: luther1912-tok1 stays frozen; only the empty Strong's slots fill. - data-prep/luther/merge-strongs.py: book-level token-stream alignment (versification-free — the Zefania edition's German numbering shifts in more places than german-numbering.tsv records), orthography fold for the old/new-spelling gap, joined-word and contraction handling. 98.3% of the source's ~350k tags transfer; unmatched words stay untagged. Fails below a 97% floor rather than writing a diverged merge. - check-luther.py claim #7: every code resolves in strongs.json, H only in the OT and G only in the NT, count floor proves the merge ran. - data-prep/strongs-de/: translate.py (Batch API machine translation of strongs_def + derivation, stdlib-only, idempotent/resumable — run with ANTHROPIC_API_KEY to produce translations.json) and build-strongs-de.py (assembles data/strongs-de.json: translated prose with English fallback, and the kjv_def slot carrying LUTHER RENDERINGS derived from the tagged corpus — 8,001 codes, frequency-ordered, alignment noise filtered). * German word study: the engine serves it, with the German lexicon slot ready - The KJV-only gates predate the German corpus carrying its own tags, so they fall where the tags make them wrong and stay where they are load-bearing: Strong's loads for either corpus (load_core_data + open), the word-study card studies German words (dictionary entry + concordance via the corpus's own occurrence index), while the KJV-token-anchored tiers — morphology, the rendering lens, same-root, the machine analytics — stay off for German, and the overlay stays KJV-only. - strongs_for(): a German reader gets data/strongs-de.json when the pack ships it (machine-translated definitions + Luther renderings), else the English dictionary. The panel labels the renderings slot for the reader's Bible ("Luther:" / "KJV:") and, on the German dictionary, carries the machine-translation caveat with a report link to the repo's issues — the new ext: link verb (https only) routes as PanelLink::External; the web opens it noopener/noreferrer. - Web pack: strongs-de.json is optional with its own germanLexicon role — installed with the German corpus (one ask covers both), fetched in stage 2 for German-installed devices, evicted after loadCoreData like its English sibling, and never downloaded by an English reader. - Android: .data-v4 → .data-v5. The v0.46.0 chronological.json addition shipped WITHOUT this bump — a ')' inside an include-list comment truncated the asset-marker test's view of the gradle list, so it never noticed. The parser now strips comments first; existing installs re-extract (and get the retagged German corpus) on next update. - Tests: the german_corpus ffi test asserts the new contract (tagged tokens, studying card, no KJV-tier leaks); a new toy-home test pins the German dictionary pick, the Luther label, the caveat + report link, and ext: routing (https only; the two run serially in CI — they flip the global language). language.spec.ts asserts the tags survive the idxcache into the web engine (mutation-killed against the restored KJV-only gate, wasm rebuilt for the mutation). * v0.47.0 * German lexicon: inline *AI-translated mark + an English-definitions opt-out - The definition paragraph carries a small faded "*KI-übersetzt" / "*AI-translated" run when the German dictionary serves — the at-a-glance mark; the full caveat + report link still close the card. - New additive config field strongsDeOff: a German reader who prefers the original English definitions flips it in Settings ▸ language ("Englische Strong-Definitionen"). Applied by strongs_for at engine open, so the toggle flushes and reloads exactly like a language change. Round-trips the config wire and the on-disk golden. - The german_corpus toy test covers the escape hatch: strongsDeOff in the config returns the English definitions with no caveat attached. * UAT round 2: picker exclusivity, chip names what's left, Explore overflow, "studied" Four fixes from on-device UAT of the combined branch: - The plans picker hides builtins whose class is already running — starting "the Bible in 90 days" while "in a year" runs could only mean abandoning the one you're on (StudyPanel offerable(); plans.classFull hint when a class is fully occupied). - The nav chip's label names what is LEFT of today (planToday remaining()), so finishing Genesis 1 of a Gen 1–4 day shows "Gen 2–4" instead of a label that never moves all evening; dwell reports also invalidate the cached plans read (session onReadingWrote) as a belt for the same surface. - Explore's cards no longer spill text past their border: the global 44px tap floor replaces the grid's automatic minimum size, so .ex-card restores min-height: auto (hazard documented in app.css). - User-facing copy says a chapter is "studied", not "swept" — 12 strings in en/de (keys unchanged); "sweep" survives only as the mechanism's internal name. e2e assertions updated to match, and the language spec's negative probe moved off "Sing"/"Singen" (a substring collision the menu merge exposed). Each behavioural fix carries a mutation-killed regression test in plans-today.spec.ts (picker filter removed, label reverted to the whole-day span, min-height restored to the floor — all three runs went red). README refresh by the maintainer rides along. * e2e: pay the menu rationalization's test debt PR #21 was merged with its web job red — nine tests still navigated the pre-rationalization chrome. All nine now walk the new one: - Study/Share are destinations (bottom bar on phones, header roles bar on desktop), not ≡ menu entries; the ≡ menu lost its .menu-host wrapper. - Backup and the bug report fold behind Settings ▸ Advanced, and details:has(pre.report) must name .diag or it also matches that new ancestor disclosure. - The share QR is the Share screen's card, not a header dialog. - "Sing" now names both the destination and the hymn's sing-mode button, so the hymnal spec clicks button.sing; the Sing destination's bar is titled by the hymnal itself. * data: the German Strong's dictionary ships data/strongs-de.json — 14,197 entries, same schema as strongs.json. Definitions and derivations are AI-translated from the English (Sonnet, sentence-by-sentence under a fixed glossary; the panel marks them *KI-übersetzt and links this repo's issues), the kjv_def slot carries Luther 1912 renderings counted from the corpus itself (8,001 codes; 8 fall back to English prose). data-prep/strongs-de/translations.json is the raw translation record build-strongs-de.py assembles from, kept for reproducibility and review. Android bundles it (gradle syncData + the asset-marker list — the .data-v5 re-extract already covers it); the web pack picks it up through the germanLexicon role wired earlier.
1 parent 96bb38c commit 2b7758e

67 files changed

Lines changed: 33268 additions & 32130 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,10 @@ jobs:
5858
# here it is the only thing proving `data/luther1912.jsonl` sits at the
5959
# KJV's verse addresses, comes back as German, and does not get handed the
6060
# KJV's Strong's or plain-English overlay.
61+
# --test-threads=1: these tests flip the process-global language and
62+
# would race each other's resets in parallel.
6163
- name: Test the German corpus
62-
run: cargo test --locked -p plumbline-ffi --lib -- --ignored german_corpus
64+
run: cargo test --locked -p plumbline-ffi --lib -- --ignored german_corpus --test-threads=1
6365
# The corpus idxcache must be REPRODUCIBLE. It is the biggest file in the
6466
# web pack and the pack manifest hashes it, so a cache that differs run to
6567
# run re-mints every `?v=` URL and makes every reader re-download the whole

BIBLIOGRAPHY.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ eBible.org's catalogue lists that KJV today as **`ENGKJV` / `eng-kjv2006`**
3333
module id `engKJV2006eb` itself no longer resolves there (checked 2026-07-29).
3434
`data/` and `bridge/` are committed to this repo, so nothing re-fetches it.
3535

36+
## The German text and its apparatus
37+
38+
| Shipped file | Contents | Source | License |
39+
|---|---|---|---|
40+
| `data/luther1912.jsonl` (text) | Luther 1912 at KJV addresses, tokenized (`luther1912-tok1`) | The Unbound Bible (Biola University) `luther_1912` module, via [kesaranb/luther1912](https://github.com/kesaranb/luther1912); built by `data-prep/luther/build-luther.py` | public domain (module metadata: "This Bible is in the Public Domain") |
41+
| `data/luther1912.jsonl` (Strong's tags) | ~344k Strong's tags merged onto the tokens | Zefania XML *Luther 1912 mit Strongs* (`SF_2022-02-27_GER_LUTH1912_Strongs_xml_220227.zip`, [zefania-sharp on SourceForge](https://sourceforge.net/projects/zefania-sharp/files/Bibles/GER/Lutherbibel/Luther%201912/)), creator/publisher www.toledot.info; merged by `data-prep/luther/merge-strongs.py` (book-level token alignment, ~98.3% of the source's tags; unmatched words stay untagged) | public domain (the file's own header: "This Text is in the Public Domain") |
42+
| `data/strongs-de.json` | German Strong's dictionary: machine-translated definitions + Luther renderings derived from the tagged corpus | translation of `data/strongs.json` by `data-prep/strongs-de/translate.py` (Claude, Batch API; labelled as machine-translated in the app) + renderings computed by `build-strongs-de.py` | **CC-BY-SA** — a derivative of openscriptures/strongs, share-alike carries over |
43+
3644
## Morphology
3745

3846
| Shipped file | Contents | Source | License |

Cargo.lock

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ default-members = [
2121
# shells — About renders it as `engine ` + this number. The workflow refuses a tag
2222
# whose version disagrees with this line (and with apps/web/package.json), so a
2323
# release ships one number, not three.
24-
version = "0.46.0"
24+
version = "0.47.0"
2525
edition = "2021"
2626
license = "MIT"
2727
authors = ["Glendon Klassen <gjklassen@proton.me>"]

README.md

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -116,35 +116,6 @@ steps, all on the phone.
116116
open **Plumbline** from your app drawer: the whole Bible is inside the file
117117
you just installed, so it opens with the network off.
118118

119-
Needs Android 8.0 or newer and a 64-bit phone (`arm64-v8a`). It declares **no
120-
INTERNET permission at all**, so it cannot phone home even by accident. One study layer rides in the
121-
web version only, because the APK doesn't carry its data file: the word-grammar
122-
gloss. Everything else is in the APK — the
123-
text, Strong's, the margin notes, the Treasury's cross-references, the
124-
cross-testament bridge, the plain-English overlay, and all of your own work.
125-
126-
**What sideloading means.** You are the one deciding to trust this file, which
127-
is why Android keeps asking. Two things follow. There is no auto-update — when
128-
a new version ships you come back to Releases and repeat the four steps; it
129-
installs over the old one and your notes, tags and threads stay where they are,
130-
because every release is signed with the same key. And take the APK only from
131-
that Releases page: a file called `plumbline-….apk` from anywhere else is not
132-
something I built. If any of this stalls on your phone, the web version above
133-
needs none of it.
134-
135-
**Checking what you downloaded** (optional). GitHub records a SHA-256 for every
136-
file attached to a release. Ask it for the APK's, hash your own copy, and
137-
compare:
138-
139-
```sh
140-
curl -s https://api.github.com/repos/glendonjklassen/plumbline/releases/latest \
141-
| grep -o 'sha256:[0-9a-f]\{64\}'
142-
sha256sum plumbline-v*-android.apk
143-
```
144-
145-
(`shasum -a 256` on macOS, `Get-FileHash` in Windows PowerShell.) The two hex
146-
strings have to match character for character.
147-
148119
## Getting started (60 seconds)
149120

150121
First launch asks who's opening the Book — **new in the faith** (a welcome
@@ -199,21 +170,6 @@ ordinary share.
199170
See **[docs/GUIDE.md](docs/GUIDE.md)** for the full tour — search syntax, the
200171
study panel tier by tier, weaves, the constellation, threads and tags.
201172

202-
## Shortcuts
203-
204-
The reading pane holds focus (click it if a dropdown steals it):
205-
206-
| Key | Action |
207-
|-----|--------|
208-
| `Up` / `Down` | scroll a few lines |
209-
| `PageUp` / `PageDown` / `Space` | scroll nearly a page |
210-
| `Home` / `End` | chapter start / end |
211-
| `Left` / `Right` (or `[` / `]`) | step chapters, rolling across book boundaries |
212-
| **`Shift`** + wheel / `Up` / `Down` / `PageUp` / `PageDown` / `Space` | **lock every pane together** (parallel reading) |
213-
| `Ctrl` + wheel, `Ctrl` `+` / `-` | zoom the body text · `Ctrl 0` resets |
214-
| `Ctrl`+click a word (or double-click) | open its Strong's study panel |
215-
| `Esc` | close the study panel / any popup (clicking outside a popup also closes it) |
216-
217173
## Threads and weaves
218174

219175
**A weave is something you find. A thread is something you make.**

apps/android/app/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,11 @@ tasks.register<Copy>("syncData") {
272272
"kjv.jsonl",
273273
"luther1912.jsonl",
274274
"strongs.json",
275+
// The German Strong's dictionary — AI-translated definitions plus
276+
// Luther renderings (data-prep/strongs-de/README). Bundled for the
277+
// same reason as the corpus: a German reader gets word study the
278+
// moment the app opens. ~3 MB compressed.
279+
"strongs-de.json",
275280
"kjv-notes.jsonl",
276281
"cross-references.tsv",
277282
"akjv.akjvb",

apps/android/app/src/main/java/dev/plumbline/MainActivity.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,14 @@ class MainActivity : ComponentActivity() {
129129
// overlay).
130130
// v3: hymnal.json.
131131
// v4: luther1912.jsonl, the German corpus.
132+
// v5: chronological.json (the reading plan's table — its
133+
// v0.46.0 addition shipped WITHOUT this bump, so existing
134+
// installs never re-extracted it), and the German corpus
135+
// re-tagged with Strong's.
132136
// `bundledDataMarkerIsBumpedForTheCurrentAssetSet` in
133137
// MainActivityTest fails the build if this marker lags the
134138
// bundled asset set.
135-
val corpus = File(home, ".data-v4")
139+
val corpus = File(home, ".data-v5")
136140
if (!corpus.exists()) {
137141
copyAsset("data", File(home, "data"), buf = buf)
138142
if ((assets.list("bridge")?.size ?: 0) > 0) {

apps/android/app/src/main/java/dev/plumbline/ui/Hymnal.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.Column
2222
import androidx.compose.foundation.layout.ExperimentalLayoutApi
2323
import androidx.compose.foundation.layout.FlowRow
2424
import androidx.compose.foundation.layout.Row
25+
import androidx.compose.foundation.layout.RowScope
2526
import androidx.compose.foundation.layout.Spacer
2627
import androidx.compose.foundation.layout.fillMaxSize
2728
import androidx.compose.foundation.layout.fillMaxWidth
@@ -103,6 +104,7 @@ fun HymnalScreen(
103104
palette: ReaderPalette,
104105
onClose: () -> Unit,
105106
onSing: (HymnSing) -> Unit,
107+
barActions: @Composable RowScope.() -> Unit = {},
106108
) {
107109
var index by remember { mutableStateOf<List<HymnalEntry>?>(null) }
108110
var filter by remember { mutableStateOf("") }
@@ -157,6 +159,7 @@ fun HymnalScreen(
157159
onBack = { if (openId != null) { openId = null; semis = 0 } else onClose() },
158160
backLabel = if (openId != null) t("hymnal.backToList") else t("bar.backToReading"),
159161
) {
162+
if (openId == null) barActions()
160163
if (openId != null && open != null) {
161164
// One hymn, two texts: the same tune sung in either language.
162165
// This is the toggle the German release grows out of.

apps/android/app/src/main/java/dev/plumbline/ui/Memorize.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,11 @@ fun MemorizeScreen(
114114
onSelectView: (MemorizeView) -> Unit = {},
115115
onDrill: (ref: String) -> Unit = {},
116116
onClose: () -> Unit,
117+
barActions: @Composable RowScope.() -> Unit = {},
117118
) {
118119
when (view) {
119120
MemorizeView.ReviewDue -> MemorizeReview(engine, palette, onClose)
120-
MemorizeView.List -> MemorizeList(engine, books, palette, onSelectView, onDrill, onClose)
121+
MemorizeView.List -> MemorizeList(engine, books, palette, onSelectView, onDrill, onClose, barActions)
121122
MemorizeView.Activity -> MemorizeActivity(engine, palette, onClose)
122123
}
123124
}
@@ -134,6 +135,7 @@ fun MemorizeList(
134135
onSelectView: (MemorizeView) -> Unit,
135136
onDrill: (ref: String) -> Unit,
136137
onClose: () -> Unit,
138+
barActions: @Composable RowScope.() -> Unit = {},
137139
) {
138140
val coverage = remember {
139141
runCatching {
@@ -155,7 +157,7 @@ fun MemorizeList(
155157
val verses = coverage?.verses ?: emptyList()
156158
val dueCount = cards.count { it.due }
157159

158-
MemFrame(t("nav.memorize"), palette, onClose) {
160+
MemFrame(t("nav.memorize"), palette, onClose, barActions) {
159161
Column(Modifier.fillMaxSize()) {
160162
// Actions: Review due (with a count) and Activity.
161163
Row(
@@ -259,11 +261,12 @@ private fun MemFrame(
259261
title: String,
260262
palette: ReaderPalette,
261263
onClose: () -> Unit,
264+
barActions: @Composable RowScope.() -> Unit = {},
262265
content: @Composable () -> Unit,
263266
) {
264267
BackHandler(onBack = onClose)
265268
Column(Modifier.fillMaxSize().background(palette.paper)) {
266-
ScreenBar(title, palette, onClose)
269+
ScreenBar(title, palette, onClose, actions = barActions)
267270
Box(Modifier.fillMaxWidth().weight(1f)) { content() }
268271
}
269272
}

apps/android/app/src/main/java/dev/plumbline/ui/NavIcons.kt

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
// The bottom navigation bar's four icons as hand-built ImageVectors (standard
2-
// Material Symbols path data). material-icons-core doesn't carry these glyphs
3-
// and material-icons-extended would bloat the unminified release APK, so the
4-
// four paths live here instead.
1+
// The bottom navigation bar's five role icons as hand-built ImageVectors
2+
// (standard Material Symbols path data). material-icons-core doesn't carry
3+
// these glyphs and material-icons-extended would bloat the unminified release
4+
// APK, so the paths live here instead. The web's NAV table carries the same
5+
// paths — parity is glyph-for-glyph, not just label-for-label.
56
//
67
// Author D (Compose UI).
78

@@ -29,17 +30,18 @@ val NavIconRead: ImageVector by lazy {
2930
)
3031
}
3132

32-
/** Material "explore" (compass) — the Explore tab. */
33-
val NavIconExplore: ImageVector by lazy {
33+
/** Material "share" — the Share tab. */
34+
val NavIconShare: ImageVector by lazy {
3435
navIcon(
35-
"nav-explore",
36-
"M12 10.9c-.61 0-1.1.49-1.1 1.1s.49 1.1 1.1 1.1c.61 0 1.1-.49 1.1-1.1s-.49-1.1-1.1-1.1z" +
37-
"M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z" +
38-
"m2.19 12.19L6 18l3.81-8.19L18 6l-3.81 8.19z",
36+
"nav-share",
37+
"M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11" +
38+
"c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81" +
39+
"C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16" +
40+
"c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92s2.92-1.31 2.92-2.92-1.31-2.92-2.92-2.92z",
3941
)
4042
}
4143

42-
/** Material "present_to_all" — the Present tab. */
44+
/** Material "present_to_all" — the Preach tab. */
4345
val NavIconPresent: ImageVector by lazy {
4446
navIcon(
4547
"nav-present",
@@ -48,16 +50,15 @@ val NavIconPresent: ImageVector by lazy {
4850
)
4951
}
5052

51-
/** Material "school" — the Memorize tab. */
52-
val NavIconMemorize: ImageVector by lazy {
53+
/** Material "school" — the Study tab. */
54+
val NavIconStudy: ImageVector by lazy {
5355
navIcon(
54-
"nav-memorize",
56+
"nav-study",
5557
"M5 13.18v4L12 21l7-3.82v-4L12 17l-7-3.82zM12 3L1 9l11 6 11-6-11-6z",
5658
)
5759
}
5860

59-
/** Material "music_note" — the Hymnal tab. The web's NAV table carries the
60-
* same path (parity is glyph-for-glyph, not just label-for-label). */
61+
/** Material "music_note" — the Sing tab. */
6162
val NavIconHymnal: ImageVector by lazy {
6263
navIcon(
6364
"nav-hymnal",

0 commit comments

Comments
 (0)