148 lines
6.5 KiB
JavaScript
148 lines
6.5 KiB
JavaScript
/*
|
|||
|
|
* 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] + ')');
|
||
|
|
}
|