Files

148 lines
6.5 KiB
JavaScript
Raw Permalink Normal View History

/*
* Regenerates bible-lexicon.js from every bible/lexicon-*.json.
* Run after editing any lexicon file.
*/
const fs = require('fs');
const path = require('path');
const files = fs.readdirSync('bible').filter((f) => /^lexicon-.*\.json$/.test(f)).sort();
const english = {}; // lang -> { English term: local form }
const fixes = {}; // lang -> { wrong local form: right local form }
const refs = {};
let total = 0;
/* A variant can INHERIT its base language's table and then diverge. Portugal
keeps "David" where Brazil writes "Davi", so por_PT cannot simply reuse
por_Latn — but it should not have to restate 85 shared entries either. */
const inheritQueue = [];
for (const f of files) {
const j = JSON.parse(fs.readFileSync(path.join('bible', f), 'utf8'));
const lang = j.language;
if (j.inherits) { inheritQueue.push(j); refs[lang] = j.reference; continue; }
refs[lang] = j.reference;
const t = {};
for (const grp of ['books', 'people', 'places', 'terms']) {
for (const [en, loc] of Object.entries(j[grp] || {})) {
if (en.startsWith('_') || en === loc) continue; // no-ops earn nothing
t[en] = loc;
}
}
english[lang] = t;
total += Object.keys(t).length;
const tf = {};
for (const [bad, good] of Object.entries(j._target_fixes || {})) {
if (bad.startsWith('_')) continue;
tf[bad] = good;
}
if (Object.keys(tf).length) fixes[lang] = tf;
}
/* Resolve inheritance after every base table is built. */
for (const j of inheritQueue) {
const from = english[j.inherits];
if (!from) throw new Error(j.language + ' inherits ' + j.inherits + ', which does not exist');
const t = Object.assign({}, from);
for (const k of (j.remove || [])) delete t[k];
for (const [en, loc] of Object.entries(j.overrides || {})) {
if (en === loc) delete t[en]; // "same as English" means: do not touch it
else t[en] = loc;
}
english[j.language] = t;
total += Object.keys(t).length;
const fb = fixes[j.inherits];
if (fb) fixes[j.language] = Object.assign({}, fb, j.targetFixes || {});
else if (j.targetFixes) fixes[j.language] = j.targetFixes;
}
const j2 = (o) => JSON.stringify(o, null, 2).split('\n').map((l, i) => i ? ' ' + l : l).join('\n');
const OUT = [
'/*',
' * bible-lexicon.js — GENERATED from bible/lexicon-*.json by _gen-lexicon.js.',
' * Do not hand-edit; edit the JSON and regenerate.',
' *',
' * TWO TABLES, and the difference is the whole safety argument.',
' *',
' * ENGLISH[] maps an ENGLISH term to the local form. It only ever fires on',
' * English text that survived translation, so there is no target-language',
' * homograph to collide with — the trap that makes a target-side lexicon',
' * dangerous (a Spanish entry for "Marca" would turn "la marca de la bestia"',
' * into "la Marcos de la bestia"). No CJK word-boundary problem either, and',
' * no noun gender or grammatical case to agree with.',
' *',
' * FIXES[] is target-side and therefore restricted to forms MEASURED wrong',
' * against a named published edition, each one checked for not being an',
' * ordinary word in that language. Never add to it by intuition.',
' *',
' * Naming authorities: ' + Object.entries(refs).map(([k, v]) => k + ' = ' + v).join('; ') + '.',
' * Verse TEXT from any copyrighted edition appears nowhere in this app; proper',
' * nouns and book titles are naming conventions, not copyrightable subject',
' * matter.',
' */',
'(function (root, factory) {',
" if (typeof module === 'object' && module.exports) module.exports = factory();",
' else root.BibleLexicon = factory();',
"}(typeof self !== 'undefined' ? self : this, function () {",
" 'use strict';",
'',
' const ENGLISH = ' + j2(english) + ';',
'',
' const FIXES = ' + j2(fixes) + ';',
'',
' /* A regional variant inherits its base language. Canonical list: NLLB_ALIAS. */',
" const VARIANT_OF = { spa_ES: 'spa_Latn', spa_MX: 'spa_Latn', por_PT: 'por_Latn', zho_Hant: 'zho_Hans' };",
' const base = (lang) => (ENGLISH[lang] ? lang : VARIANT_OF[lang]);',
'',
' function forLanguage(lang) { return ENGLISH[base(lang)] || null; }',
'',
' /*',
' * WHOLE-UTTERANCE NAME LOOKUP — the deterministic guarantee.',
' *',
' * A bare name is the worst input this model receives. MEASURED into',
' * Spanish: "Peter." came back as "\u00bfQu\u00e9 es eso?" and "Jesus." as',
' * "\u00bfQu\u00e9 quieres?" — the name GONE, replaced by invented dialogue.',
' * Into French, "Habakkuk." became "Habakuk est mort" (Habakkuk is dead).',
' * A preacher listing names one at a time produced a run of questions with',
' * no names in them at all.',
' *',
' * The cause is ONE CHARACTER: the terminal period. Without it the model',
' * translates the term ("Peter" -> "Pedro"); with it, it reads a complete',
' * conversational turn and invents a reply. preach.js strips the period for',
' * short utterances, which fixes most of it — this is the guarantee for the',
' * rest, because a name we know should never depend on the model at all.',
' *',
' * Matched on the bare word, punctuation and case stripped, and ONLY when',
' * the utterance is that name and nothing else — an ordinary sentence',
' * containing the name is untouched.',
' */',
' function wholeUtterance(text, lang) {',
' const t = ENGLISH[base(lang)];',
' if (!t) return null;',
' const bare = String(text == null ? \'\' : text)',
' .replace(/^[\\s"\\u2018\\u201c(\\[\\\']+/, \'\')',
' .replace(/[\\s.!?,;:"\\u2019\\u201d)\\]\\\']+$/, \'\')',
' .trim();',
' if (!bare) return null;',
' if (Object.prototype.hasOwnProperty.call(t, bare)) return t[bare];',
' const lower = bare.toLowerCase();',
' for (const k of Object.keys(t)) if (k.toLowerCase() === lower) return t[k];',
' return null;',
' }',
'',
' function fixesFor(lang) { return FIXES[base(lang)] || null; }',
' function languages() { return Object.keys(ENGLISH); }',
'',
' return { forLanguage: forLanguage, fixesFor: fixesFor,',
' wholeUtterance: wholeUtterance, languages: languages,',
' ENGLISH: ENGLISH, FIXES: FIXES };',
'}));'
].join('\n');
fs.writeFileSync('bible-lexicon.js', OUT);
console.log('wrote bible-lexicon.js — ' + files.length + ' languages, ' + total + ' English entries');
for (const [l, t] of Object.entries(english)) {
console.log(' ' + l.padEnd(10) + String(Object.keys(t).length).padStart(4) + ' entries, ' +
Object.keys(fixes[l] || {}).length + ' target fixes (' + refs[l] + ')');
}