Fix Greek suggest: use local macula-greek concordance instead of broken API

The previous implementation fetched SBLGNT from bible.helloao.org using
the wrong translation ID and an API that returns raw text strings, not
per-word Strong's data — so it always silently returned nothing.

Now uses a bundled NT Strong's concordance (public/nt-strongs-concordance.json,
~908 KB) derived from the Clear-Bible macula-greek SBLGNT dataset. This maps
each verse (book/chapter/verse) to the exact Strong's numbers that appear in it,
making the suggest feature accurate and offline-capable after first load.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmert
2026-05-28 12:44:40 -04:00
parent 53790437b3
commit cda93a68eb
2 changed files with 32 additions and 25 deletions
File diff suppressed because one or more lines are too long
+31 -25
View File
@@ -910,6 +910,23 @@ useEffect(() => {
})); }));
}; };
// Cache for the NT Strong's concordance (verse → Strong's numbers), loaded once
const _concordanceRef = useRef(null);
const _concordanceLoadingRef = useRef(null);
const loadNtConcordance = async () => {
if (_concordanceRef.current) return _concordanceRef.current;
if (_concordanceLoadingRef.current) return _concordanceLoadingRef.current;
_concordanceLoadingRef.current = fetch('/nt-strongs-concordance.json')
.then((res) => res.json())
.then((data) => {
_concordanceRef.current = data;
_concordanceLoadingRef.current = null;
return data;
});
return _concordanceLoadingRef.current;
};
const suggestGreekWordsForChunk = async (chunkId) => { const suggestGreekWordsForChunk = async (chunkId) => {
const chunk = allChunks.find((c) => c.id === chunkId); const chunk = allChunks.find((c) => c.id === chunkId);
const chapter = project?.chapters.find((ch) => const chapter = project?.chapters.find((ch) =>
@@ -923,47 +940,36 @@ useEffect(() => {
} }
setSuggestingGreekForChunkId(chunkId); setSuggestingGreekForChunkId(chunkId);
try { try {
const res = await fetch( const [concordance, dict] = await Promise.all([loadNtConcordance(), loadGreekDict()]);
`https://bible.helloao.org/api/SBLGNT/${chapter.bookAbbrev}/${chapter.chapter}.json` const bookData = concordance[chapter.bookAbbrev] ?? {};
); const chapterData = bookData[chapter.chapter] ?? {};
if (!res.ok) throw new Error('Could not fetch interlinear data.');
const data = await res.json(); // Collect unique Strong's numbers across the verse range
const verses = Array.isArray(data?.verses) const strongsInRange = new Set();
? data.verses for (let v = chunk.startVerse; v <= chunk.endVerse; v++) {
: (data?.chapter?.content ?? []).filter((item) => item?.type === 'verse'); const verseStrongs = chapterData[String(v)] ?? [];
const strongsInRange = new Map(); for (const s of verseStrongs) strongsInRange.add(s);
for (const verse of verses) {
const verseNum = verse.number ?? verse.verse;
if (verseNum < chunk.startVerse || verseNum > chunk.endVerse) continue;
const words = Array.isArray(verse.content) ? verse.content : [];
for (const word of words) {
const strongs = word?.strongs ?? word?.strong;
const text = word?.text ?? word?.greek ?? '';
const translit = word?.transliteration ?? word?.translit ?? '';
if (!strongs || !/^G\d+$/i.test(strongs)) continue;
const key = strongs.toUpperCase();
if (!strongsInRange.has(key)) strongsInRange.set(key, { strongs: key, text, translit });
}
} }
if (strongsInRange.size === 0) { if (strongsInRange.size === 0) {
setStatusMessage("No Strong's data found for this passage."); setStatusMessage("No Strong's data found for this passage.");
window.setTimeout(() => setStatusMessage(''), 3000); window.setTimeout(() => setStatusMessage(''), 3000);
return; return;
} }
const dict = await loadGreekDict();
const existingNumbers = new Set( const existingNumbers = new Set(
chunk.greekWords.map((w) => w.strongNumber.toUpperCase()).filter(Boolean) chunk.greekWords.map((w) => w.strongNumber.toUpperCase()).filter(Boolean)
); );
const newWords = []; const newWords = [];
for (const [strongKey, meta] of strongsInRange) { for (const strongKey of strongsInRange) {
if (existingNumbers.has(strongKey)) continue; if (existingNumbers.has(strongKey)) continue;
const entry = dict[strongKey]; const entry = dict[strongKey];
newWords.push({ newWords.push({
id: makeId(), id: makeId(),
query: strongKey, query: strongKey,
strongNumber: strongKey, strongNumber: strongKey,
lexeme: entry?.lemma ?? meta.text ?? '', lexeme: entry?.lemma ?? '',
transliteration: entry?.translit ?? meta.translit ?? '', transliteration: entry?.translit ?? '',
partOfSpeech: '', partOfSpeech: '',
shortDefinition: entry?.kjv_def ?? 'No definition found.', shortDefinition: entry?.kjv_def ?? 'No definition found.',
definitionHtml: entry ? buildGreekDefinitionHtml(strongKey, entry) : '', definitionHtml: entry ? buildGreekDefinitionHtml(strongKey, entry) : '',