Load translations from LibreBible catalog

This commit is contained in:
2026-07-12 11:11:54 -05:00
parent 735e247477
commit b91912649c
8 changed files with 305 additions and 61 deletions
+130 -29
View File
@@ -1,5 +1,14 @@
import { invoke } from '@tauri-apps/api/core'
import type { CommentaryEntry, DictionaryEntry, Footnote, LibraryState, StrongLink, Tag, Verse } from './types'
import type {
CommentaryEntry,
DictionaryEntry,
Footnote,
LibraryState,
StrongLink,
Tag,
TranslationResource,
Verse,
} from './types'
type TauriWindow = Window &
typeof globalThis & {
@@ -8,9 +17,11 @@ type TauriWindow = Window &
const browserTagKey = 'libre-study-browser-tags'
const libreBibleDevRoot = '/@fs/W:/libre-bible-data/packages/json'
const defaultTranslationId = 'kjv-eng-kjv2006'
type ImportedVerse = {
id: string
translation_id: string
book: string
chapter: number
verse: number
@@ -28,11 +39,28 @@ type ImportedFootnote = {
id: number
verse_id: string
note_index: number
note_type?: string
label?: string
caller: string
reference: string
text: string
}
type ImportedCatalog = {
resources: Array<{
id: string
resource_type: string
title: string
short_title?: string
abbreviation?: string
license?: {
name?: string
attribution?: string
restricted_notes?: string
}
}>
}
type ImportedStrongEntry = {
strongs_number: string
language: 'hebrew' | 'greek'
@@ -85,9 +113,11 @@ type BrowserStrongData = {
verseIdsByStrong: Map<string, Set<string>>
}
let browserLibraryCache: Promise<Verse[]> | null = null
let browserStrongDataCache: Promise<BrowserStrongData> | null = null
let browserFootnotesCache: Promise<Map<string, Footnote[]>> | null = null
let browserCatalogCache: Promise<ImportedCatalog> | null = null
const browserLibraryCache = new Map<string, Promise<Verse[]>>()
const browserStrongDataCache = new Map<string, Promise<BrowserStrongData>>()
const browserFootnotesCache = new Map<string, Promise<Map<string, Footnote[]>>>()
const browserStudyNotesCache = new Map<string, Promise<Map<string, Footnote[]>>>()
let browserCommentaryCache: Promise<Map<string, CommentaryEntry[]>> | null = null
let browserDictionaryCache: Promise<Map<string, DictionaryEntry>> | null = null
const browserVerseSourceIds = new Map<number, string>()
@@ -230,26 +260,33 @@ function writeBrowserTags(tagsByVerse: Record<number, Tag[]>) {
localStorage.setItem(browserTagKey, JSON.stringify(tagsByVerse))
}
async function loadBrowserVerses() {
browserLibraryCache ??= loadLibreBibleDevVerses().catch((error) => {
async function loadBrowserVersesForTranslation(translationId = defaultTranslationId) {
if (!browserLibraryCache.has(translationId)) {
browserLibraryCache.set(translationId, loadLibreBibleDevVerses(translationId).catch((error) => {
console.warn('Falling back to browser seed data.', error)
return fallbackBrowserVerses
})
}))
}
const verses = await browserLibraryCache
const strongData = await loadBrowserStrongData().catch((error) => {
const verses = await browserLibraryCache.get(translationId)!
const strongData = await loadBrowserStrongData(translationId).catch((error) => {
console.warn('Unable to load browser Strong data.', error)
return null
})
const footnotes = await loadBrowserFootnotes().catch((error) => {
const footnotes = await loadBrowserFootnotes(translationId).catch((error) => {
console.warn('Unable to load browser footnotes.', error)
return new Map<string, Footnote[]>()
})
const studyNotes = await loadBrowserStudyNotes(translationId).catch((error) => {
console.warn('Unable to load browser study notes.', error)
return new Map<string, Footnote[]>()
})
const tagsByVerse = readBrowserTags()
return verses.map((verse) => ({
...verse,
strongs: strongData?.linksByVerse.get(browserVerseKey(verse)) ?? verse.strongs,
footnotes: footnotes.get(browserVerseKey(verse)) ?? verse.footnotes ?? [],
studyNotes: studyNotes.get(browserVerseKey(verse)) ?? verse.studyNotes ?? [],
tags: tagsByVerse[verse.id] ?? [],
}))
}
@@ -264,24 +301,41 @@ function allBrowserTags() {
return [...unique.values()].sort((a, b) => a.label.localeCompare(b.label))
}
export async function getLibraryState(): Promise<LibraryState> {
export async function getTranslations(): Promise<TranslationResource[]> {
if (inTauri()) return []
const catalog = await loadBrowserCatalog()
return catalog.resources
.filter((resource) => resource.resource_type === 'translation')
.map((resource) => ({
id: resource.id,
title: resource.title,
shortTitle: resource.short_title ?? resource.title,
abbreviation: resource.abbreviation ?? resource.short_title ?? resource.title,
licenseName: resource.license?.name ?? '',
attribution: resource.license?.attribution ?? '',
restrictions: resource.license?.restricted_notes ?? '',
}))
}
export async function getLibraryState(translationId = defaultTranslationId): Promise<LibraryState> {
if (inTauri()) {
return invoke<LibraryState>('get_library_state')
}
return {
verses: await loadBrowserVerses(),
verses: await loadBrowserVersesForTranslation(translationId),
tags: allBrowserTags(),
}
}
export async function searchVerses(query: string): Promise<Verse[]> {
export async function searchVerses(query: string, translationId = defaultTranslationId): Promise<Verse[]> {
if (inTauri()) {
return invoke<Verse[]>('search_verses', { query })
}
const trimmed = query.trim().toLowerCase()
const verses = await loadBrowserVerses()
const verses = await loadBrowserVersesForTranslation(translationId)
if (!trimmed) return verses
if (trimmed.startsWith('tag:')) {
@@ -292,7 +346,7 @@ export async function searchVerses(query: string): Promise<Verse[]> {
}
if (/^[gh]\d+$/.test(trimmed)) {
const strongData = await loadBrowserStrongData()
const strongData = await loadBrowserStrongData(translationId)
const matchingVerseIds = strongData.verseIdsByStrong.get(trimmed.toUpperCase()) ?? new Set()
return verses.filter((verse) =>
matchingVerseIds.has(browserVerseKey(verse)),
@@ -306,7 +360,12 @@ export async function searchVerses(query: string): Promise<Verse[]> {
)
}
export async function addTagToVerse(verseId: number, label: string, color: string) {
export async function addTagToVerse(
verseId: number,
label: string,
color: string,
translationId = defaultTranslationId,
) {
if (inTauri()) {
return invoke<Verse>('add_tag_to_verse', { verseId, label, color })
}
@@ -318,7 +377,7 @@ export async function addTagToVerse(verseId: number, label: string, color: strin
tagsByVerse[verseId] = existing ? current : [...current, tag]
writeBrowserTags(tagsByVerse)
const verse = (await loadBrowserVerses()).find((item) => item.id === verseId)
const verse = (await loadBrowserVersesForTranslation(translationId)).find((item) => item.id === verseId)
if (!verse) throw new Error(`Verse ${verseId} was not found.`)
return verse
}
@@ -352,9 +411,14 @@ export async function getDictionaryEntriesForTerms(terms: string[]): Promise<Dic
return entries
}
async function loadLibreBibleDevVerses(): Promise<Verse[]> {
async function loadBrowserCatalog(): Promise<ImportedCatalog> {
browserCatalogCache ??= fetchJson<ImportedCatalog>(`${libreBibleDevRoot}/catalog.json`)
return browserCatalogCache
}
async function loadLibreBibleDevVerses(translationId = defaultTranslationId): Promise<Verse[]> {
const importedVerses = await fetchJsonl<ImportedVerse>(
`${libreBibleDevRoot}/kjv-eng-kjv2006/verses.jsonl`,
`${libreBibleDevRoot}/${translationId}/verses.jsonl`,
)
browserVerseSourceIds.clear()
@@ -369,6 +433,7 @@ async function loadLibreBibleDevVerses(): Promise<Verse[]> {
text: verse.text,
strongs: [],
footnotes: [],
studyNotes: [],
tags: [],
}
})
@@ -442,16 +507,39 @@ async function loadLibreBibleDevDictionary(): Promise<Map<string, DictionaryEntr
)
}
async function loadBrowserFootnotes(): Promise<Map<string, Footnote[]>> {
browserFootnotesCache ??= loadLibreBibleDevFootnotes()
async function loadBrowserFootnotes(translationId = defaultTranslationId): Promise<Map<string, Footnote[]>> {
if (!browserFootnotesCache.has(translationId)) {
browserFootnotesCache.set(translationId, loadLibreBibleDevFootnotes(translationId))
}
return browserFootnotesCache
return browserFootnotesCache.get(translationId)!
}
async function loadLibreBibleDevFootnotes(): Promise<Map<string, Footnote[]>> {
async function loadBrowserStudyNotes(translationId = defaultTranslationId): Promise<Map<string, Footnote[]>> {
if (!browserStudyNotesCache.has(translationId)) {
browserStudyNotesCache.set(translationId, loadLibreBibleDevStudyNotes(translationId))
}
return browserStudyNotesCache.get(translationId)!
}
async function loadLibreBibleDevFootnotes(translationId = defaultTranslationId): Promise<Map<string, Footnote[]>> {
const importedFootnotes = await fetchJsonl<ImportedFootnote>(
`${libreBibleDevRoot}/kjv-eng-kjv2006/footnotes.jsonl`,
`${libreBibleDevRoot}/${translationId}/footnotes.jsonl`,
)
return mapImportedNotes(importedFootnotes)
}
async function loadLibreBibleDevStudyNotes(translationId = defaultTranslationId): Promise<Map<string, Footnote[]>> {
const importedFootnotes = await fetchJsonl<ImportedFootnote>(
`${libreBibleDevRoot}/${translationId}/study-notes.jsonl`,
)
return mapImportedNotes(importedFootnotes)
}
function mapImportedNotes(importedFootnotes: ImportedFootnote[]): Map<string, Footnote[]> {
const footnotes = new Map<string, Footnote[]>()
for (const note of importedFootnotes) {
@@ -463,6 +551,8 @@ async function loadLibreBibleDevFootnotes(): Promise<Map<string, Footnote[]>> {
caller: note.caller,
reference: note.reference,
text: note.text,
label: note.label,
noteType: note.note_type,
})
footnotes.set(note.verse_id, current)
}
@@ -470,15 +560,17 @@ async function loadLibreBibleDevFootnotes(): Promise<Map<string, Footnote[]>> {
return footnotes
}
async function loadBrowserStrongData(): Promise<BrowserStrongData> {
browserStrongDataCache ??= loadLibreBibleDevStrongData()
async function loadBrowserStrongData(translationId = defaultTranslationId): Promise<BrowserStrongData> {
if (!browserStrongDataCache.has(translationId)) {
browserStrongDataCache.set(translationId, loadLibreBibleDevStrongData(translationId))
}
return browserStrongDataCache
return browserStrongDataCache.get(translationId)!
}
async function loadLibreBibleDevStrongData(): Promise<BrowserStrongData> {
async function loadLibreBibleDevStrongData(translationId = defaultTranslationId): Promise<BrowserStrongData> {
const [importedLinks, importedEntries] = await Promise.all([
fetchJsonl<ImportedStrongLink>(`${libreBibleDevRoot}/kjv-eng-kjv2006/strongs-links.jsonl`),
fetchJsonl<ImportedStrongLink>(`${libreBibleDevRoot}/${translationId}/strongs-links.jsonl`),
fetchJsonl<ImportedStrongEntry>(`${libreBibleDevRoot}/strongs-open-scriptures/entries.jsonl`),
])
@@ -551,3 +643,12 @@ async function fetchJsonl<T>(url: string): Promise<T[]> {
.filter((line) => line.trim().length > 0)
.map((line) => JSON.parse(line) as T)
}
async function fetchJson<T>(url: string): Promise<T> {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`Unable to load ${url}: ${response.status} ${response.statusText}`)
}
return response.json() as Promise<T>
}