From 6dddb2b6dd943cf392faadf415f9bd7b557f0941 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 04:28:33 +0200 Subject: [PATCH 1/2] feat(api): add complete translation download endpoint --- packages/helloao-tools/generation/api.ts | 165 +++++++++++++++++++++++ 1 file changed, 165 insertions(+) diff --git a/packages/helloao-tools/generation/api.ts b/packages/helloao-tools/generation/api.ts index ebaefe4..43bf430 100644 --- a/packages/helloao-tools/generation/api.ts +++ b/packages/helloao-tools/generation/api.ts @@ -1,4 +1,6 @@ import { + ChapterContent, + ChapterFootnote, Commentary, CommentaryBook, CommentaryBookChapter, @@ -96,6 +98,12 @@ export interface ApiOutput { */ datasetBookChapters?: ApiDatasetBookChapter[]; + /** + * The complete translation data for each translation. + * This maps to the /api/:translationId/complete.json endpoint. + */ + translationComplete: ApiTranslationComplete[]; + /** * The path prefix that the API should use. */ @@ -240,6 +248,107 @@ export interface ApiTranslation extends Translation { * Null or undefined if the language doesn't have an english name. */ languageEnglishName?: string; + + /** + * The API link for downloading the complete translation as a single JSON file. + */ + completeTranslationApiLink: string; +} + +/** + * Defines the complete translation download data. + * Maps to the /api/:translationId/complete.json endpoint. + */ +export interface ApiTranslationComplete { + /** + * The translation metadata. + */ + translation: ApiTranslation; + + /** + * The complete list of books with all their chapters. + */ + books: ApiTranslationCompleteBook[]; +} + +/** + * A book in the complete translation download. + */ +export interface ApiTranslationCompleteBook { + /** + * The ID of the book. + */ + id: string; + + /** + * The name of the book from the translation. + */ + name: string; + + /** + * The common name for the book. + */ + commonName: string; + + /** + * The title of the book. + */ + title: string | null; + + /** + * The order of the book. + */ + order: number; + + /** + * The number of chapters in the book. + */ + numberOfChapters: number; + + /** + * The total number of verses in the book. + */ + totalNumberOfVerses: number; + + /** + * Whether the book is apocryphal. + */ + isApocryphal?: boolean; + + /** + * The complete list of chapters with all content. + */ + chapters: ApiTranslationCompleteChapter[]; +} + +/** + * A chapter in the complete translation download. + */ +export interface ApiTranslationCompleteChapter { + /** + * The chapter number. + */ + number: number; + + /** + * The chapter content (verses, headings, line breaks). + */ + content: ChapterContent[]; + + /** + * The footnotes for this chapter. + */ + footnotes: ChapterFootnote[]; + + /** + * The number of verses in this chapter. + */ + numberOfVerses: number; + + /** + * The audio links for this chapter. + */ + audioLinks: TranslationBookChapterAudioLinks; } /** @@ -714,6 +823,7 @@ export function generateApiForDataset( translationBooks: [], translationBookChapters: [], translationBookChapterAudio: [], + translationComplete: [], availableCommentaries: { commentaries: [], }, @@ -746,6 +856,10 @@ export function generateApiForDataset( translation.id, apiPathPrefix ), + completeTranslationApiLink: completeTranslationApiLink( + translation.id, + apiPathPrefix + ), numberOfBooks, totalNumberOfChapters: 0, totalNumberOfVerses: 0, @@ -897,6 +1011,34 @@ export function generateApiForDataset( api.availableTranslations.translations.push(apiTranslation); api.translationBooks.push(translationBooks); + + // Build the complete translation data for download + const completeTranslation: ApiTranslationComplete = { + translation: apiTranslation, + books: translationBooks.books.map((book) => { + const bookChapters = translationChapters.filter( + (ch) => ch.book.id === book.id + ); + return { + id: book.id, + name: book.name, + commonName: book.commonName, + title: book.title, + order: book.order, + numberOfChapters: book.numberOfChapters, + totalNumberOfVerses: book.totalNumberOfVerses, + isApocryphal: book.isApocryphal, + chapters: bookChapters.map((ch) => ({ + number: ch.chapter.number, + content: ch.chapter.content, + footnotes: ch.chapter.footnotes, + numberOfVerses: ch.numberOfVerses, + audioLinks: ch.thisChapterAudioLinks, + })), + }; + }), + }; + api.translationComplete.push(completeTranslation); } for (let { books, profiles, ...commentary } of dataset.commentaries) { @@ -1240,6 +1382,16 @@ export function generateFilesForApi(api: ApiOutput): OutputFile[] { files.push(downloadedFile(audio.link, audio.originalUrl)); } + // Generate complete translation download files + for (let complete of api.translationComplete) { + files.push( + jsonFile( + complete.translation.completeTranslationApiLink, + complete + ) + ); + } + files.push( jsonFile( `${api.pathPrefix}/api/available_commentaries.json`, @@ -1337,6 +1489,19 @@ export function listOfBooksApiLink( return `${prefix}/api/${translationId}/books.json`; } +/** + * Gets the API Link for the complete translation download endpoint. + * @param translationId The ID of the translation. + * @param prefix The path prefix. + * @returns + */ +export function completeTranslationApiLink( + translationId: string, + prefix: string = '' +): string { + return `${prefix}/api/${translationId}/complete.json`; +} + /** * Gets the API Link for the list of books endpoint for a commentary. * @param commentaryId The ID of the commentary. From 86c329d8a77f572717408840bcadaef7c1708922 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 04:52:28 +0200 Subject: [PATCH 2/2] feat(beblia): add Bible translation import from Beblia XML format - Add Beblia XML parser to parse translation metadata and Bible content - Implement language detection from translation names supporting 10 languages - Add multilingual book names data for Romanian, Spanish, French, German, Portuguese, Russian, Chinese, Italian, Polish, and Dutch - Add CLI commands: list-beblia and import-beblia for discovering and importing translations - Integrate BebliaXmlParser into dataset generation pipeline - Support import of 1045+ Bible translations from Beblia/Holy-Bible-XML-Format repository - Add XML file support to parser infrastructure --- packages/helloao-cli/actions.ts | 216 +++++ packages/helloao-cli/beblia.ts | 246 ++++++ packages/helloao-cli/cli.ts | 28 + packages/helloao-cli/files.ts | 7 +- packages/helloao-cli/language-detection.ts | 325 +++++++ .../generation/book-names-data.ts | 809 ++++++++++++++++++ .../helloao-tools/generation/book-order.ts | 29 + .../helloao-tools/generation/common-types.ts | 2 +- packages/helloao-tools/generation/dataset.ts | 21 + .../helloao-tools/parser/beblia-xml-parser.ts | 297 +++++++ packages/helloao-tools/parser/index.ts | 1 + 11 files changed, 1979 insertions(+), 2 deletions(-) create mode 100644 packages/helloao-cli/beblia.ts create mode 100644 packages/helloao-cli/language-detection.ts create mode 100644 packages/helloao-tools/generation/book-names-data.ts create mode 100644 packages/helloao-tools/parser/beblia-xml-parser.ts diff --git a/packages/helloao-cli/actions.ts b/packages/helloao-cli/actions.ts index a717c15..cca9511 100644 --- a/packages/helloao-cli/actions.ts +++ b/packages/helloao-cli/actions.ts @@ -1308,6 +1308,222 @@ export async function listEBibleTranslations( } } +// ============================================================================ +// Beblia XML Import Functions +// ============================================================================ + +import { + fetchBebliaIndex, + downloadBebliaTranslation, + filterByLanguage, + filterBySearch, + getTranslationStats, + BebliaTranslation, +} from './beblia.js'; +import { + BebliaXmlParser, +} from '@helloao/tools/parser/beblia-xml-parser.js'; + +export interface ListBebliaOptions { + language?: string; +} + +export interface ImportBebliaOptions { + all?: boolean; + language?: string; + overwrite?: boolean; + db?: string | null; +} + +/** + * Lists available Beblia translations. + * + * @param searchTerm - Optional search term to filter translations. + * @param options - Options for filtering. + */ +export async function listBebliaTranslations( + searchTerm?: string, + options: ListBebliaOptions = {} +): Promise { + const logger = log.getLogger(); + logger.log('Fetching Beblia translation index...'); + + const index = await fetchBebliaIndex(); + let translations = index.translations; + + // Apply language filter + if (options.language) { + translations = filterByLanguage(translations, options.language); + logger.log( + `Filtered to ${translations.length} translations for language: ${options.language}` + ); + } + + // Apply search filter + if (searchTerm) { + translations = filterBySearch(translations, searchTerm); + logger.log( + `Found ${translations.length} translations matching "${searchTerm}":` + ); + } else { + logger.log(`Total ${translations.length} available translations:`); + } + + logger.log('Format: [Filename] | [Language] | [Name]'); + logger.log('─'.repeat(80)); + + for (const t of translations) { + logger.log( + `${t.filename.padEnd(30)} | ${t.detectedLanguage.padEnd(5)} | ${t.name}` + ); + } + + if (translations.length === 0) { + logger.log('No translations found. Try a different search term.'); + } + + // Show language stats if no filters applied + if (!searchTerm && !options.language) { + logger.log(''); + logger.log('─'.repeat(80)); + logger.log('Language distribution:'); + const stats = getTranslationStats(index.translations); + const sortedStats = [...stats.entries()].sort((a, b) => b[1] - a[1]); + for (const [lang, count] of sortedStats.slice(0, 20)) { + logger.log(` ${lang}: ${count} translations`); + } + if (sortedStats.length > 20) { + logger.log(` ... and ${sortedStats.length - 20} more languages`); + } + } +} + +/** + * Downloads and prepares Beblia translations for import. + * + * @param outputDir - Directory to save the translations. + * @param translationFilters - Specific translations to download (by name or filename). + * @param options - Options for the import. + */ +export async function importBebliaTranslations( + outputDir: string, + translationFilters: string[], + options: ImportBebliaOptions = {} +): Promise { + const logger = log.getLogger(); + logger.log('Fetching Beblia translation index...'); + + const index = await fetchBebliaIndex(); + let translations: BebliaTranslation[]; + + if (options.all) { + translations = index.translations; + logger.log(`Preparing to download all ${translations.length} translations...`); + } else if (options.language) { + translations = filterByLanguage(index.translations, options.language); + logger.log( + `Found ${translations.length} translations for language: ${options.language}` + ); + } else if (translationFilters.length > 0) { + translations = index.translations.filter((t) => + translationFilters.some( + (filter) => + t.filename.toLowerCase().includes(filter.toLowerCase()) || + t.name.toLowerCase().includes(filter.toLowerCase()) + ) + ); + logger.log( + `Found ${translations.length} translations matching filters: ${translationFilters.join(', ')}` + ); + } else { + logger.log( + 'No translations specified. Use --all, --language , or provide translation names.' + ); + logger.log('Examples:'); + logger.log(' npm run cli import-beblia ./sources/beblia --all'); + logger.log(' npm run cli import-beblia ./sources/beblia --language ron'); + logger.log(' npm run cli import-beblia ./sources/beblia RomanianBible'); + return; + } + + if (translations.length === 0) { + logger.log('No translations matched the criteria.'); + return; + } + + // Create output directory + await mkdir(outputDir, { recursive: true }); + + let downloaded = 0; + let skipped = 0; + let errors = 0; + + for (const translation of translations) { + try { + // Create translation-specific directory + const translationId = translation.filename.replace(/\.xml$/i, ''); + const translationDir = path.resolve(outputDir, translationId); + + // Check if already exists + const metaPath = path.resolve(translationDir, 'metadata.json'); + if (!options.overwrite && existsSync(metaPath)) { + logger.log(`Skipping ${translationId} (already exists)`); + skipped++; + continue; + } + + await mkdir(translationDir, { recursive: true }); + + // Download XML file + logger.log(`Downloading: ${translation.name}`); + const xml = await downloadBebliaTranslation(translation); + + // Save XML file + const xmlPath = path.resolve(translationDir, translation.filename); + await writeFile(xmlPath, xml, 'utf-8'); + + // Extract metadata from XML and create meta.json + const parser = new DOMParser(); + globalThis.DOMParser = DOMParser as any; + const bebliaParser = new BebliaXmlParser(parser as any); + const metadata = bebliaParser.parseMetadataOnly(xml); + + const meta: InputTranslationMetadata = { + id: translationId, + name: metadata.translation || translation.name, + englishName: metadata.version || metadata.translation || translation.name, + language: translation.detectedLanguage, + direction: translation.textDirection, + shortName: translationId.slice(0, 10).toUpperCase(), + licenseUrl: translation.sourceLink || + metadata.status || + 'https://github.com/Beblia/Holy-Bible-XML-Format', + website: metadata.link || + translation.sourceLink || + 'https://github.com/Beblia/Holy-Bible-XML-Format', + }; + + await writeFile(metaPath, JSON.stringify(meta, null, 2), 'utf-8'); + + logger.log(` Saved to: ${translationDir}`); + downloaded++; + } catch (err) { + logger.error(`Error downloading ${translation.name}:`, err); + errors++; + } + } + + logger.log(''); + logger.log('─'.repeat(80)); + logger.log('BEBLIA IMPORT SUMMARY:'); + logger.log(` Downloaded: ${downloaded}`); + logger.log(` Skipped: ${skipped}`); + logger.log(` Errors: ${errors}`); + logger.log(''); + logger.log('To import these translations into the database, run:'); + logger.log(` npm run cli import-translations ${outputDir}`); +} + /** * Generates the translation files directly from the translation stored in the given input directory. * @param input The input directory that the translation is stored in. diff --git a/packages/helloao-cli/beblia.ts b/packages/helloao-cli/beblia.ts new file mode 100644 index 0000000..abb89e9 --- /dev/null +++ b/packages/helloao-cli/beblia.ts @@ -0,0 +1,246 @@ +import { DOMParser } from 'linkedom'; +import { log } from '@helloao/tools'; +import { + detectLanguageFromName, + getTextDirection, +} from './language-detection.js'; + +/** + * URL for the Beblia bibles.xml index file. + * This is hosted on the radio-crestin fork which maintains the index. + */ +export const BEBLIA_INDEX_URL = + 'https://github.com/radio-crestin/Holy-Bible-XML-Format/releases/latest/download/bibles.xml'; + +/** + * Base URL for downloading individual Bible XML files. + */ +export const BEBLIA_RAW_BASE_URL = + 'https://github.com/radio-crestin/Holy-Bible-XML-Format/raw/refs/tags/v1.0.1/data'; + +/** + * Represents a Bible translation entry from the Beblia index. + */ +export interface BebliaTranslation { + /** Full name of the translation */ + name: string; + /** Filename (e.g., "RomanianBible.xml") */ + filename: string; + /** Direct download URL */ + downloadUrl: string; + /** Copyright information if available */ + copyright?: string; + /** Source link if available */ + sourceLink?: string; + /** Auto-detected ISO 639-3 language code */ + detectedLanguage: string; + /** Text direction (ltr or rtl) */ + textDirection: 'ltr' | 'rtl'; +} + +/** + * Represents the Beblia translations index. + */ +export interface BebliaIndex { + /** Total number of translations available */ + totalCount: number; + /** Repository URL */ + repositoryUrl: string; + /** Index version tag */ + version: string; + /** List of all translations */ + translations: BebliaTranslation[]; +} + +/** + * Fetches and parses the Beblia translations index. + * + * @returns The parsed index with all available translations. + */ +export async function fetchBebliaIndex(): Promise { + const logger = log.getLogger(); + logger.log('Fetching Beblia index from:', BEBLIA_INDEX_URL); + + const response = await fetch(BEBLIA_INDEX_URL, { + redirect: 'follow', + }); + + if (!response.ok) { + throw new Error( + `Failed to fetch Beblia index: ${response.status} ${response.statusText}` + ); + } + + const xml = await response.text(); + return parseBebliaIndex(xml); +} + +/** + * Parses the Beblia index XML content. + * + * @param xml - The raw XML content of the index file. + * @returns The parsed index. + */ +export function parseBebliaIndex(xml: string): BebliaIndex { + const parser = new DOMParser(); + const doc = parser.parseFromString(xml, 'text/xml'); + const root = doc.documentElement; + + // Parse metadata + const metadata = root.querySelector('metadata'); + const totalCount = parseInt( + metadata?.querySelector('total_translations')?.textContent || '0', + 10 + ); + const repositoryUrl = + metadata?.querySelector('repository')?.textContent || + 'https://github.com/Beblia/Holy-Bible-XML-Format'; + const version = metadata?.querySelector('tag')?.textContent || 'v1.0.0'; + + // Parse translations + const translations: BebliaTranslation[] = []; + const translationElements = root.querySelectorAll( + 'translations > translation' + ); + + for (const el of translationElements) { + const name = el.querySelector('name')?.textContent?.trim() || ''; + const filename = el.querySelector('filename')?.textContent?.trim() || ''; + const downloadUrl = + el.querySelector('download_url')?.textContent?.trim() || ''; + const copyright = + el.querySelector('copyright')?.textContent?.trim() || undefined; + const sourceLink = + el.querySelector('source_link')?.textContent?.trim() || undefined; + + if (!name || !filename) { + continue; // Skip invalid entries + } + + // Detect language from name + const detectedLanguage = detectLanguageFromName(name); + const textDirection = getTextDirection(detectedLanguage); + + translations.push({ + name, + filename, + downloadUrl: + downloadUrl || `${BEBLIA_RAW_BASE_URL}/${filename}`, + copyright, + sourceLink, + detectedLanguage, + textDirection, + }); + } + + return { + totalCount: totalCount || translations.length, + repositoryUrl, + version, + translations, + }; +} + +/** + * Downloads a single Bible translation XML file. + * + * @param translation - The translation to download. + * @returns The raw XML content. + */ +export async function downloadBebliaTranslation( + translation: BebliaTranslation +): Promise { + const logger = log.getLogger(); + logger.log('Downloading translation:', translation.name); + + const response = await fetch(translation.downloadUrl, { + redirect: 'follow', + }); + + if (!response.ok) { + throw new Error( + `Failed to download translation ${translation.name}: ${response.status} ${response.statusText}` + ); + } + + return response.text(); +} + +/** + * Downloads a Bible translation by filename. + * + * @param filename - The filename (e.g., "RomanianBible.xml"). + * @returns The raw XML content. + */ +export async function downloadBebliaTranslationByFilename( + filename: string +): Promise { + const url = `${BEBLIA_RAW_BASE_URL}/${filename}`; + const logger = log.getLogger(); + logger.log('Downloading from:', url); + + const response = await fetch(url, { + redirect: 'follow', + }); + + if (!response.ok) { + throw new Error( + `Failed to download ${filename}: ${response.status} ${response.statusText}` + ); + } + + return response.text(); +} + +/** + * Filters translations by language code. + * + * @param translations - The list of translations to filter. + * @param languageCode - The ISO 639 language code to filter by. + * @returns Translations matching the language code. + */ +export function filterByLanguage( + translations: BebliaTranslation[], + languageCode: string +): BebliaTranslation[] { + const normalized = languageCode.toLowerCase(); + return translations.filter( + (t) => t.detectedLanguage.toLowerCase() === normalized + ); +} + +/** + * Filters translations by search term. + * + * @param translations - The list of translations to filter. + * @param searchTerm - The search term to filter by (matches name or filename). + * @returns Translations matching the search term. + */ +export function filterBySearch( + translations: BebliaTranslation[], + searchTerm: string +): BebliaTranslation[] { + const normalized = searchTerm.toLowerCase(); + return translations.filter( + (t) => + t.name.toLowerCase().includes(normalized) || + t.filename.toLowerCase().includes(normalized) + ); +} + +/** + * Gets statistics about the available translations. + * + * @param translations - The list of translations. + * @returns Statistics about language distribution. + */ +export function getTranslationStats( + translations: BebliaTranslation[] +): Map { + const stats = new Map(); + for (const t of translations) { + const count = stats.get(t.detectedLanguage) || 0; + stats.set(t.detectedLanguage, count + 1); + } + return stats; +} diff --git a/packages/helloao-cli/cli.ts b/packages/helloao-cli/cli.ts index 64462fb..db0c553 100644 --- a/packages/helloao-cli/cli.ts +++ b/packages/helloao-cli/cli.ts @@ -18,6 +18,8 @@ import { importTranslations, initDb, listEBibleTranslations, + listBebliaTranslations, + importBebliaTranslations, sourceTranslations, uploadTestTranslation, uploadTestTranslations, @@ -599,6 +601,32 @@ async function start() { await Promise.all(promises); }); + // Beblia XML format commands + program + .command('list-beblia [search]') + .description( + 'List available Beblia translations. Optionally filter by search term.' + ) + .option('--language ', 'Filter by language code (e.g., ron, spa)') + .action(async (search: string | undefined, options: any) => { + await listBebliaTranslations(search, options); + }); + + program + .command('import-beblia [translations...]') + .description( + 'Downloads and imports Beblia XML translations to the specified directory.' + ) + .option('--all', 'Download all available translations') + .option('--language ', 'Filter by language code (e.g., ron, spa)') + .option('--overwrite', 'Overwrite existing files') + .action(async (dir: string, translations: string[], options: any) => { + await importBebliaTranslations(dir, translations, { + ...program.opts(), + ...options, + }); + }); + await program.parseAsync(process.argv); } diff --git a/packages/helloao-cli/files.ts b/packages/helloao-cli/files.ts index 351dc6d..927d7dc 100644 --- a/packages/helloao-cli/files.ts +++ b/packages/helloao-cli/files.ts @@ -274,7 +274,8 @@ export async function loadTranslationFiles( extname(f) === '.usfm' || extname(f) === '.usx' || extname(f) === '.json' || - extname(f) === '.codex' + extname(f) === '.codex' || + extname(f) === '.xml' ); if (usfmFiles.length <= 0) { @@ -546,6 +547,10 @@ function getFileType(ext: string): InputTranslationFile['fileType'] | null { return 'json'; case 'codex': return 'json'; + case 'xml': + // For .xml files, we default to beblia-xml format + // USX files typically use .usx extension + return 'beblia-xml'; default: return null; } diff --git a/packages/helloao-cli/language-detection.ts b/packages/helloao-cli/language-detection.ts new file mode 100644 index 0000000..e28c5e7 --- /dev/null +++ b/packages/helloao-cli/language-detection.ts @@ -0,0 +1,325 @@ +/** + * Language detection module for Bible translations. + * Detects ISO 639-3 language codes from translation names. + */ + +/** + * Language pattern definitions. + * Each entry is [RegExp pattern, ISO 639-3 code, text direction] + */ +const LANGUAGE_PATTERNS: [RegExp, string, 'ltr' | 'rtl'][] = [ + // Major European languages + [/\benglish\b|\bkjv\b|\bniv\b|\besv\b|\bnasb\b|\bnkjv\b|\basv\b|\bweb\b|\bnet\b/i, 'eng', 'ltr'], + [/\bromanian\b|\bromân|\bcornilescu\b|\bvdc\b/i, 'ron', 'ltr'], + [/\bspanish\b|\bespa[nñ]ol\b|\breina.?valera\b|\brvr\b|\bnvi\b/i, 'spa', 'ltr'], + [/\bfrench\b|\bfran[cç]ais\b|\blouis.?segond\b|\bsegond\b/i, 'fra', 'ltr'], + [/\bgerman\b|\bdeutsch\b|\bluther\b|\bschlachter\b|\belberfelder\b/i, 'deu', 'ltr'], + [/\bportuguese\b|\bportugu[eê]s\b|\balmeida\b|\bnvi.?pt\b/i, 'por', 'ltr'], + [/\bitalian\b|\bitaliano\b|\bdiodati\b|\briveduta\b/i, 'ita', 'ltr'], + [/\bdutch\b|\bnederlands\b|\bstatenvertaling\b|\bnbv\b/i, 'nld', 'ltr'], + [/\bpolish\b|\bpolski\b|\bwarszawska\b|\bgda[nń]ska\b/i, 'pol', 'ltr'], + [/\bczech\b|\b[čc]e[sš]k[áy]\b|\bkralick[áa]\b/i, 'ces', 'ltr'], + [/\bslovak\b|\bslovensk[áy]\b/i, 'slk', 'ltr'], + [/\bhungarian\b|\bmagyar\b|\bk[áa]roli\b/i, 'hun', 'ltr'], + [/\bromanian\b|\bromân\b|\bcornilescu\b/i, 'ron', 'ltr'], + [/\bbulgarian\b|\bбългарск/i, 'bul', 'ltr'], + [/\bserbian\b|\bсрпски\b|\bsrpski\b/i, 'srp', 'ltr'], + [/\bcroatian\b|\bhrvatski\b/i, 'hrv', 'ltr'], + [/\bslovenian\b|\bslovenski\b/i, 'slv', 'ltr'], + [/\bukrainian\b|\bукраїнськ/i, 'ukr', 'ltr'], + [/\brussian\b|\bрусск|\bsynodal\b|\bрусский\b/i, 'rus', 'ltr'], + [/\bbelarusian\b|\bбеларуск/i, 'bel', 'ltr'], + [/\blithuanian\b|\blietuvi[uų]\b/i, 'lit', 'ltr'], + [/\blatvian\b|\blatvie[sš]u\b/i, 'lav', 'ltr'], + [/\bestonian\b|\beesti\b/i, 'est', 'ltr'], + [/\bfinnish\b|\bsuomi\b|\bsuomalainen\b/i, 'fin', 'ltr'], + [/\bswedish\b|\bsvensk\b/i, 'swe', 'ltr'], + [/\bnorwegian\b|\bnorsk\b/i, 'nor', 'ltr'], + [/\bdanish\b|\bdansk\b/i, 'dan', 'ltr'], + [/\bicelandic\b|\bíslen[sz]k/i, 'isl', 'ltr'], + [/\bgreek\b|\bελληνικ[άή]/i, 'ell', 'ltr'], + [/\balbanian\b|\bshqip/i, 'sqi', 'ltr'], + [/\bmacedonian\b|\bмакедонски/i, 'mkd', 'ltr'], + + // Asian languages + [/\bchinese\b|\b中[文国國]\b|\b简体\b|\b繁[體体]\b|\b和合本\b/i, 'zho', 'ltr'], + [/\bjapanese\b|\b日本語\b|\b口語訳\b/i, 'jpn', 'ltr'], + [/\bkorean\b|\b한국어\b|\b한글\b|\b개역\b/i, 'kor', 'ltr'], + [/\bvietnamese\b|\btiếng.?việt\b|\bviệt\b/i, 'vie', 'ltr'], + [/\bthai\b|\bไทย\b/i, 'tha', 'ltr'], + [/\bindonesian\b|\bbahasa.?indonesia\b|\bterjemahan.?baru\b/i, 'ind', 'ltr'], + [/\bmalay\b|\bmelayu\b|\bbahasa.?melayu\b/i, 'msa', 'ltr'], + [/\bfilipino\b|\btagalog\b|\bpilipino\b/i, 'tgl', 'ltr'], + [/\bhindi\b|\bहिन्दी\b|\bहिंदी\b/i, 'hin', 'ltr'], + [/\bbengali\b|\bbangla\b|\bবাংলা\b/i, 'ben', 'ltr'], + [/\btamil\b|\bதமிழ்\b/i, 'tam', 'ltr'], + [/\btelugu\b|\bతెలుగు\b/i, 'tel', 'ltr'], + [/\bmarathi\b|\bमराठी\b/i, 'mar', 'ltr'], + [/\bgujarati\b|\bગુજરાતી\b/i, 'guj', 'ltr'], + [/\bkannada\b|\bಕನ್ನಡ\b/i, 'kan', 'ltr'], + [/\bmalayalam\b|\bമലയാളം\b/i, 'mal', 'ltr'], + [/\bpunjabi\b|\bਪੰਜਾਬੀ\b/i, 'pan', 'ltr'], + [/\bnepali\b|\bनेपाली\b/i, 'nep', 'ltr'], + [/\bsinhala\b|\bසිංහල\b/i, 'sin', 'ltr'], + [/\bburmese\b|\bမြန်မာ\b/i, 'mya', 'ltr'], + [/\bkhmer\b|\bកម្ពុជា\b|\bcambodian\b/i, 'khm', 'ltr'], + [/\blao\b|\bລາວ\b/i, 'lao', 'ltr'], + [/\bmongolian\b|\bмонгол\b/i, 'mon', 'ltr'], + + // Middle Eastern languages (RTL) + [/\barabic\b|\bعربي\b|\bالعربية\b|\bvan.?dyck\b/i, 'arb', 'rtl'], + [/\bhebrew\b|\bעברית\b|\bיהוד/i, 'heb', 'rtl'], + [/\bpersian\b|\bfarsi\b|\bفارسی\b/i, 'fas', 'rtl'], + [/\burdu\b|\bاردو\b/i, 'urd', 'rtl'], + [/\bpashto\b|\bپښتو\b/i, 'pus', 'rtl'], + [/\bkurdish\b|\bکوردی\b/i, 'kur', 'rtl'], + + // African languages + [/\bswahili\b|\bkiswahili\b/i, 'swa', 'ltr'], + [/\bamharic\b|\bአማርኛ\b/i, 'amh', 'ltr'], + [/\bhausa\b/i, 'hau', 'ltr'], + [/\byoruba\b|\byorùbá\b/i, 'yor', 'ltr'], + [/\bigbo\b/i, 'ibo', 'ltr'], + [/\bzulu\b|\bisizulu\b/i, 'zul', 'ltr'], + [/\bxhosa\b|\bisixhosa\b/i, 'xho', 'ltr'], + [/\bafrikaans\b/i, 'afr', 'ltr'], + [/\bmalagasy\b/i, 'mlg', 'ltr'], + [/\bsomali\b|\bsoomaali\b/i, 'som', 'ltr'], + [/\btigrinya\b|\bትግርኛ\b/i, 'tir', 'ltr'], + [/\boromo\b|\bafaan.?oromoo\b/i, 'orm', 'ltr'], + [/\bshona\b|\bchishona\b/i, 'sna', 'ltr'], + [/\bnyanja\b|\bchichewa\b|\bchewa\b/i, 'nya', 'ltr'], + [/\btswana\b|\bsetswana\b/i, 'tsn', 'ltr'], + [/\bsotho\b|\bsesotho\b/i, 'sot', 'ltr'], + [/\brwanda\b|\bkinyarwanda\b/i, 'kin', 'ltr'], + [/\bkirundi\b|\brundi\b/i, 'run', 'ltr'], + [/\blingala\b/i, 'lin', 'ltr'], + [/\bwolof\b/i, 'wol', 'ltr'], + [/\bfula\b|\bfulfulde\b|\bpulaar\b/i, 'ful', 'ltr'], + + // Other languages + [/\bturkish\b|\btürkçe\b/i, 'tur', 'ltr'], + [/\bazerbaijani\b|\bazərbaycan\b/i, 'aze', 'ltr'], + [/\bkazakh\b|\bқазақ\b/i, 'kaz', 'ltr'], + [/\buzbe[ck]\b|\bo['ʻ]zbek\b/i, 'uzb', 'ltr'], + [/\bgeorgian\b|\bქართული\b/i, 'kat', 'ltr'], + [/\barmenian\b|\bհայերdelays\b/i, 'hye', 'ltr'], + [/\bhaitia?n?.?creole\b|\bkreyòl\b/i, 'hat', 'ltr'], + [/\bcatalan\b|\bcatalà\b/i, 'cat', 'ltr'], + [/\bbasque\b|\beuskara\b/i, 'eus', 'ltr'], + [/\bgalician\b|\bgalego\b/i, 'glg', 'ltr'], + [/\bwelsh\b|\bcymraeg\b/i, 'cym', 'ltr'], + [/\birish\b|\bgaeilge\b/i, 'gle', 'ltr'], + [/\bscots.?gaelic\b|\bgàidhlig\b/i, 'gla', 'ltr'], + [/\bmaltese\b|\bmalti\b/i, 'mlt', 'ltr'], + [/\besperanto\b/i, 'epo', 'ltr'], + [/\blatin\b|\bvulgata?\b|\blatina\b/i, 'lat', 'ltr'], + [/\baceh\b|\bacehnese\b/i, 'ace', 'ltr'], + [/\bjavanese\b|\bjawa\b|\bbasa.?jawa\b/i, 'jav', 'ltr'], + [/\bsundanese\b|\bsunda\b/i, 'sun', 'ltr'], + [/\bbatak\b/i, 'btk', 'ltr'], + [/\bcebuano\b|\bsinugboanon\b/i, 'ceb', 'ltr'], + [/\bilocano\b|\biloko\b/i, 'ilo', 'ltr'], + [/\bhiligaynon\b|\bilonggo\b/i, 'hil', 'ltr'], + [/\bwaray\b/i, 'war', 'ltr'], + [/\bbikol\b|\bbicol\b/i, 'bik', 'ltr'], + [/\bpapua\b|\btok.?pisin\b/i, 'tpi', 'ltr'], + [/\bmaori\b|\bmāori\b/i, 'mri', 'ltr'], + [/\bsamoan\b|\bsā?moa\b/i, 'smo', 'ltr'], + [/\btongan\b|\blea.?faka-?tonga\b/i, 'ton', 'ltr'], + [/\bfijian\b|\bvosa.?vakaviti\b/i, 'fij', 'ltr'], + [/\btahitian\b|\breo.?tahiti\b/i, 'tah', 'ltr'], + [/\bhawaiian\b|\bʻōlelo.?hawaiʻi\b/i, 'haw', 'ltr'], +]; + +/** + * RTL (Right-to-Left) language codes + */ +const RTL_LANGUAGES = new Set([ + 'arb', 'ara', 'ar', // Arabic + 'heb', 'he', // Hebrew + 'fas', 'per', 'fa', // Persian/Farsi + 'urd', 'ur', // Urdu + 'pus', 'ps', // Pashto + 'kur', 'ku', // Kurdish + 'uig', 'ug', // Uyghur + 'syr', // Syriac + 'div', 'dv', // Dhivehi + 'yid', 'yi', // Yiddish +]); + +/** + * Detects the ISO 639-3 language code from a translation name. + * + * @param name - The translation name (e.g., "Romanian VDC 1924", "Aceh Language") + * @returns The detected ISO 639-3 language code, or 'eng' as fallback + */ +export function detectLanguageFromName(name: string): string { + const normalizedName = name.toLowerCase(); + + for (const [pattern, code] of LANGUAGE_PATTERNS) { + if (pattern.test(normalizedName)) { + return code; + } + } + + // Default fallback to English + return 'eng'; +} + +/** + * Checks if a language code represents an RTL (Right-to-Left) language. + * + * @param langCode - The ISO 639 language code (2 or 3 letter) + * @returns true if the language is RTL, false otherwise + */ +export function isRtlLanguage(langCode: string): boolean { + return RTL_LANGUAGES.has(langCode.toLowerCase()); +} + +/** + * Gets the text direction for a language code. + * + * @param langCode - The ISO 639 language code + * @returns 'rtl' for RTL languages, 'ltr' for LTR languages + */ +export function getTextDirection(langCode: string): 'ltr' | 'rtl' { + return isRtlLanguage(langCode) ? 'rtl' : 'ltr'; +} + +/** + * Normalizes a language code to ISO 639-3 format. + * Handles common variations and mappings. + * + * @param code - The input language code + * @returns The normalized ISO 639-3 code + */ +export function normalizeLanguageCode(code: string): string { + const normalized = code.toLowerCase().trim(); + + // Common ISO 639-1 to ISO 639-3 mappings + const mappings: Record = { + en: 'eng', + ro: 'ron', + es: 'spa', + fr: 'fra', + de: 'deu', + pt: 'por', + it: 'ita', + nl: 'nld', + pl: 'pol', + cs: 'ces', + sk: 'slk', + hu: 'hun', + bg: 'bul', + sr: 'srp', + hr: 'hrv', + sl: 'slv', + uk: 'ukr', + ru: 'rus', + be: 'bel', + lt: 'lit', + lv: 'lav', + et: 'est', + fi: 'fin', + sv: 'swe', + no: 'nor', + da: 'dan', + is: 'isl', + el: 'ell', + sq: 'sqi', + mk: 'mkd', + zh: 'zho', + ja: 'jpn', + ko: 'kor', + vi: 'vie', + th: 'tha', + id: 'ind', + ms: 'msa', + tl: 'tgl', + hi: 'hin', + bn: 'ben', + ta: 'tam', + te: 'tel', + mr: 'mar', + gu: 'guj', + kn: 'kan', + ml: 'mal', + pa: 'pan', + ne: 'nep', + si: 'sin', + my: 'mya', + km: 'khm', + lo: 'lao', + mn: 'mon', + ar: 'arb', + he: 'heb', + fa: 'fas', + ur: 'urd', + ps: 'pus', + ku: 'kur', + sw: 'swa', + am: 'amh', + ha: 'hau', + yo: 'yor', + ig: 'ibo', + zu: 'zul', + xh: 'xho', + af: 'afr', + mg: 'mlg', + so: 'som', + ti: 'tir', + om: 'orm', + sn: 'sna', + ny: 'nya', + tn: 'tsn', + st: 'sot', + rw: 'kin', + rn: 'run', + ln: 'lin', + wo: 'wol', + ff: 'ful', + tr: 'tur', + az: 'aze', + kk: 'kaz', + uz: 'uzb', + ka: 'kat', + hy: 'hye', + ht: 'hat', + ca: 'cat', + eu: 'eus', + gl: 'glg', + cy: 'cym', + ga: 'gle', + gd: 'gla', + mt: 'mlt', + eo: 'epo', + la: 'lat', + jv: 'jav', + su: 'sun', + mi: 'mri', + sm: 'smo', + to: 'ton', + fj: 'fij', + ty: 'tah', + }; + + return mappings[normalized] ?? normalized; +} + +/** + * Attempts to extract language information from a filename. + * + * @param filename - The filename (e.g., "RomanianBible.xml", "AcehBible.xml") + * @returns The detected language code or null if not detected + */ +export function detectLanguageFromFilename(filename: string): string | null { + // Remove extension and common suffixes + const baseName = filename + .replace(/\.xml$/i, '') + .replace(/Bible$/i, '') + .replace(/_/g, ' '); + + const detected = detectLanguageFromName(baseName); + return detected !== 'eng' ? detected : null; +} diff --git a/packages/helloao-tools/generation/book-names-data.ts b/packages/helloao-tools/generation/book-names-data.ts new file mode 100644 index 0000000..5c42d31 --- /dev/null +++ b/packages/helloao-tools/generation/book-names-data.ts @@ -0,0 +1,809 @@ +/** + * Book names in multiple languages. + * This file contains translations of Bible book names for use with Beblia XML imports. + * + * Sources: + * - Bible SuperSearch (https://www.biblesupersearch.com/bible-downloads/) + * - Various Bible translation projects + * + * Language codes follow ISO 639-3 standard. + */ + +export interface BookNameEntry { + commonName: string; +} + +type BookNameMap = Map; + +/** + * Romanian book names + */ +export const romanianBookMap: BookNameMap = new Map([ + ['GEN', { commonName: 'Geneza' }], + ['EXO', { commonName: 'Exodul' }], + ['LEV', { commonName: 'Leviticul' }], + ['NUM', { commonName: 'Numeri' }], + ['DEU', { commonName: 'Deuteronomul' }], + ['JOS', { commonName: 'Iosua' }], + ['JDG', { commonName: 'Judecătorii' }], + ['RUT', { commonName: 'Rut' }], + ['1SA', { commonName: '1 Samuel' }], + ['2SA', { commonName: '2 Samuel' }], + ['1KI', { commonName: '1 Împărați' }], + ['2KI', { commonName: '2 Împărați' }], + ['1CH', { commonName: '1 Cronici' }], + ['2CH', { commonName: '2 Cronici' }], + ['EZR', { commonName: 'Ezra' }], + ['NEH', { commonName: 'Neemia' }], + ['EST', { commonName: 'Estera' }], + ['JOB', { commonName: 'Iov' }], + ['PSA', { commonName: 'Psalmii' }], + ['PRO', { commonName: 'Proverbele' }], + ['ECC', { commonName: 'Eclesiastul' }], + ['SNG', { commonName: 'Cântarea Cântărilor' }], + ['ISA', { commonName: 'Isaia' }], + ['JER', { commonName: 'Ieremia' }], + ['LAM', { commonName: 'Plângerile lui Ieremia' }], + ['EZK', { commonName: 'Ezechiel' }], + ['DAN', { commonName: 'Daniel' }], + ['HOS', { commonName: 'Osea' }], + ['JOL', { commonName: 'Ioel' }], + ['AMO', { commonName: 'Amos' }], + ['OBA', { commonName: 'Obadia' }], + ['JON', { commonName: 'Iona' }], + ['MIC', { commonName: 'Mica' }], + ['NAM', { commonName: 'Naum' }], + ['HAB', { commonName: 'Habacuc' }], + ['ZEP', { commonName: 'Țefania' }], + ['HAG', { commonName: 'Hagai' }], + ['ZEC', { commonName: 'Zaharia' }], + ['MAL', { commonName: 'Maleahi' }], + ['MAT', { commonName: 'Matei' }], + ['MRK', { commonName: 'Marcu' }], + ['LUK', { commonName: 'Luca' }], + ['JHN', { commonName: 'Ioan' }], + ['ACT', { commonName: 'Faptele Apostolilor' }], + ['ROM', { commonName: 'Romani' }], + ['1CO', { commonName: '1 Corinteni' }], + ['2CO', { commonName: '2 Corinteni' }], + ['GAL', { commonName: 'Galateni' }], + ['EPH', { commonName: 'Efeseni' }], + ['PHP', { commonName: 'Filipeni' }], + ['COL', { commonName: 'Coloseni' }], + ['1TH', { commonName: '1 Tesaloniceni' }], + ['2TH', { commonName: '2 Tesaloniceni' }], + ['1TI', { commonName: '1 Timotei' }], + ['2TI', { commonName: '2 Timotei' }], + ['TIT', { commonName: 'Tit' }], + ['PHM', { commonName: 'Filimon' }], + ['HEB', { commonName: 'Evrei' }], + ['JAS', { commonName: 'Iacov' }], + ['1PE', { commonName: '1 Petru' }], + ['2PE', { commonName: '2 Petru' }], + ['1JN', { commonName: '1 Ioan' }], + ['2JN', { commonName: '2 Ioan' }], + ['3JN', { commonName: '3 Ioan' }], + ['JUD', { commonName: 'Iuda' }], + ['REV', { commonName: 'Apocalipsa' }], +]); + +/** + * Spanish book names + */ +export const spanishBookMap: BookNameMap = new Map([ + ['GEN', { commonName: 'Génesis' }], + ['EXO', { commonName: 'Éxodo' }], + ['LEV', { commonName: 'Levítico' }], + ['NUM', { commonName: 'Números' }], + ['DEU', { commonName: 'Deuteronomio' }], + ['JOS', { commonName: 'Josué' }], + ['JDG', { commonName: 'Jueces' }], + ['RUT', { commonName: 'Rut' }], + ['1SA', { commonName: '1 Samuel' }], + ['2SA', { commonName: '2 Samuel' }], + ['1KI', { commonName: '1 Reyes' }], + ['2KI', { commonName: '2 Reyes' }], + ['1CH', { commonName: '1 Crónicas' }], + ['2CH', { commonName: '2 Crónicas' }], + ['EZR', { commonName: 'Esdras' }], + ['NEH', { commonName: 'Nehemías' }], + ['EST', { commonName: 'Ester' }], + ['JOB', { commonName: 'Job' }], + ['PSA', { commonName: 'Salmos' }], + ['PRO', { commonName: 'Proverbios' }], + ['ECC', { commonName: 'Eclesiastés' }], + ['SNG', { commonName: 'Cantares' }], + ['ISA', { commonName: 'Isaías' }], + ['JER', { commonName: 'Jeremías' }], + ['LAM', { commonName: 'Lamentaciones' }], + ['EZK', { commonName: 'Ezequiel' }], + ['DAN', { commonName: 'Daniel' }], + ['HOS', { commonName: 'Oseas' }], + ['JOL', { commonName: 'Joel' }], + ['AMO', { commonName: 'Amós' }], + ['OBA', { commonName: 'Abdías' }], + ['JON', { commonName: 'Jonás' }], + ['MIC', { commonName: 'Miqueas' }], + ['NAM', { commonName: 'Nahúm' }], + ['HAB', { commonName: 'Habacuc' }], + ['ZEP', { commonName: 'Sofonías' }], + ['HAG', { commonName: 'Hageo' }], + ['ZEC', { commonName: 'Zacarías' }], + ['MAL', { commonName: 'Malaquías' }], + ['MAT', { commonName: 'Mateo' }], + ['MRK', { commonName: 'Marcos' }], + ['LUK', { commonName: 'Lucas' }], + ['JHN', { commonName: 'Juan' }], + ['ACT', { commonName: 'Hechos' }], + ['ROM', { commonName: 'Romanos' }], + ['1CO', { commonName: '1 Corintios' }], + ['2CO', { commonName: '2 Corintios' }], + ['GAL', { commonName: 'Gálatas' }], + ['EPH', { commonName: 'Efesios' }], + ['PHP', { commonName: 'Filipenses' }], + ['COL', { commonName: 'Colosenses' }], + ['1TH', { commonName: '1 Tesalonicenses' }], + ['2TH', { commonName: '2 Tesalonicenses' }], + ['1TI', { commonName: '1 Timoteo' }], + ['2TI', { commonName: '2 Timoteo' }], + ['TIT', { commonName: 'Tito' }], + ['PHM', { commonName: 'Filemón' }], + ['HEB', { commonName: 'Hebreos' }], + ['JAS', { commonName: 'Santiago' }], + ['1PE', { commonName: '1 Pedro' }], + ['2PE', { commonName: '2 Pedro' }], + ['1JN', { commonName: '1 Juan' }], + ['2JN', { commonName: '2 Juan' }], + ['3JN', { commonName: '3 Juan' }], + ['JUD', { commonName: 'Judas' }], + ['REV', { commonName: 'Apocalipsis' }], +]); + +/** + * French book names + */ +export const frenchBookMap: BookNameMap = new Map([ + ['GEN', { commonName: 'Genèse' }], + ['EXO', { commonName: 'Exode' }], + ['LEV', { commonName: 'Lévitique' }], + ['NUM', { commonName: 'Nombres' }], + ['DEU', { commonName: 'Deutéronome' }], + ['JOS', { commonName: 'Josué' }], + ['JDG', { commonName: 'Juges' }], + ['RUT', { commonName: 'Ruth' }], + ['1SA', { commonName: '1 Samuel' }], + ['2SA', { commonName: '2 Samuel' }], + ['1KI', { commonName: '1 Rois' }], + ['2KI', { commonName: '2 Rois' }], + ['1CH', { commonName: '1 Chroniques' }], + ['2CH', { commonName: '2 Chroniques' }], + ['EZR', { commonName: 'Esdras' }], + ['NEH', { commonName: 'Néhémie' }], + ['EST', { commonName: 'Esther' }], + ['JOB', { commonName: 'Job' }], + ['PSA', { commonName: 'Psaumes' }], + ['PRO', { commonName: 'Proverbes' }], + ['ECC', { commonName: 'Ecclésiaste' }], + ['SNG', { commonName: 'Cantique des Cantiques' }], + ['ISA', { commonName: 'Ésaïe' }], + ['JER', { commonName: 'Jérémie' }], + ['LAM', { commonName: 'Lamentations' }], + ['EZK', { commonName: 'Ézéchiel' }], + ['DAN', { commonName: 'Daniel' }], + ['HOS', { commonName: 'Osée' }], + ['JOL', { commonName: 'Joël' }], + ['AMO', { commonName: 'Amos' }], + ['OBA', { commonName: 'Abdias' }], + ['JON', { commonName: 'Jonas' }], + ['MIC', { commonName: 'Michée' }], + ['NAM', { commonName: 'Nahum' }], + ['HAB', { commonName: 'Habacuc' }], + ['ZEP', { commonName: 'Sophonie' }], + ['HAG', { commonName: 'Aggée' }], + ['ZEC', { commonName: 'Zacharie' }], + ['MAL', { commonName: 'Malachie' }], + ['MAT', { commonName: 'Matthieu' }], + ['MRK', { commonName: 'Marc' }], + ['LUK', { commonName: 'Luc' }], + ['JHN', { commonName: 'Jean' }], + ['ACT', { commonName: 'Actes' }], + ['ROM', { commonName: 'Romains' }], + ['1CO', { commonName: '1 Corinthiens' }], + ['2CO', { commonName: '2 Corinthiens' }], + ['GAL', { commonName: 'Galates' }], + ['EPH', { commonName: 'Éphésiens' }], + ['PHP', { commonName: 'Philippiens' }], + ['COL', { commonName: 'Colossiens' }], + ['1TH', { commonName: '1 Thessaloniciens' }], + ['2TH', { commonName: '2 Thessaloniciens' }], + ['1TI', { commonName: '1 Timothée' }], + ['2TI', { commonName: '2 Timothée' }], + ['TIT', { commonName: 'Tite' }], + ['PHM', { commonName: 'Philémon' }], + ['HEB', { commonName: 'Hébreux' }], + ['JAS', { commonName: 'Jacques' }], + ['1PE', { commonName: '1 Pierre' }], + ['2PE', { commonName: '2 Pierre' }], + ['1JN', { commonName: '1 Jean' }], + ['2JN', { commonName: '2 Jean' }], + ['3JN', { commonName: '3 Jean' }], + ['JUD', { commonName: 'Jude' }], + ['REV', { commonName: 'Apocalypse' }], +]); + +/** + * German book names + */ +export const germanBookMap: BookNameMap = new Map([ + ['GEN', { commonName: '1. Mose' }], + ['EXO', { commonName: '2. Mose' }], + ['LEV', { commonName: '3. Mose' }], + ['NUM', { commonName: '4. Mose' }], + ['DEU', { commonName: '5. Mose' }], + ['JOS', { commonName: 'Josua' }], + ['JDG', { commonName: 'Richter' }], + ['RUT', { commonName: 'Rut' }], + ['1SA', { commonName: '1. Samuel' }], + ['2SA', { commonName: '2. Samuel' }], + ['1KI', { commonName: '1. Könige' }], + ['2KI', { commonName: '2. Könige' }], + ['1CH', { commonName: '1. Chronik' }], + ['2CH', { commonName: '2. Chronik' }], + ['EZR', { commonName: 'Esra' }], + ['NEH', { commonName: 'Nehemia' }], + ['EST', { commonName: 'Ester' }], + ['JOB', { commonName: 'Hiob' }], + ['PSA', { commonName: 'Psalmen' }], + ['PRO', { commonName: 'Sprüche' }], + ['ECC', { commonName: 'Prediger' }], + ['SNG', { commonName: 'Hohelied' }], + ['ISA', { commonName: 'Jesaja' }], + ['JER', { commonName: 'Jeremia' }], + ['LAM', { commonName: 'Klagelieder' }], + ['EZK', { commonName: 'Hesekiel' }], + ['DAN', { commonName: 'Daniel' }], + ['HOS', { commonName: 'Hosea' }], + ['JOL', { commonName: 'Joel' }], + ['AMO', { commonName: 'Amos' }], + ['OBA', { commonName: 'Obadja' }], + ['JON', { commonName: 'Jona' }], + ['MIC', { commonName: 'Micha' }], + ['NAM', { commonName: 'Nahum' }], + ['HAB', { commonName: 'Habakuk' }], + ['ZEP', { commonName: 'Zefanja' }], + ['HAG', { commonName: 'Haggai' }], + ['ZEC', { commonName: 'Sacharja' }], + ['MAL', { commonName: 'Maleachi' }], + ['MAT', { commonName: 'Matthäus' }], + ['MRK', { commonName: 'Markus' }], + ['LUK', { commonName: 'Lukas' }], + ['JHN', { commonName: 'Johannes' }], + ['ACT', { commonName: 'Apostelgeschichte' }], + ['ROM', { commonName: 'Römer' }], + ['1CO', { commonName: '1. Korinther' }], + ['2CO', { commonName: '2. Korinther' }], + ['GAL', { commonName: 'Galater' }], + ['EPH', { commonName: 'Epheser' }], + ['PHP', { commonName: 'Philipper' }], + ['COL', { commonName: 'Kolosser' }], + ['1TH', { commonName: '1. Thessalonicher' }], + ['2TH', { commonName: '2. Thessalonicher' }], + ['1TI', { commonName: '1. Timotheus' }], + ['2TI', { commonName: '2. Timotheus' }], + ['TIT', { commonName: 'Titus' }], + ['PHM', { commonName: 'Philemon' }], + ['HEB', { commonName: 'Hebräer' }], + ['JAS', { commonName: 'Jakobus' }], + ['1PE', { commonName: '1. Petrus' }], + ['2PE', { commonName: '2. Petrus' }], + ['1JN', { commonName: '1. Johannes' }], + ['2JN', { commonName: '2. Johannes' }], + ['3JN', { commonName: '3. Johannes' }], + ['JUD', { commonName: 'Judas' }], + ['REV', { commonName: 'Offenbarung' }], +]); + +/** + * Portuguese book names + */ +export const portugueseBookMap: BookNameMap = new Map([ + ['GEN', { commonName: 'Gênesis' }], + ['EXO', { commonName: 'Êxodo' }], + ['LEV', { commonName: 'Levítico' }], + ['NUM', { commonName: 'Números' }], + ['DEU', { commonName: 'Deuteronômio' }], + ['JOS', { commonName: 'Josué' }], + ['JDG', { commonName: 'Juízes' }], + ['RUT', { commonName: 'Rute' }], + ['1SA', { commonName: '1 Samuel' }], + ['2SA', { commonName: '2 Samuel' }], + ['1KI', { commonName: '1 Reis' }], + ['2KI', { commonName: '2 Reis' }], + ['1CH', { commonName: '1 Crônicas' }], + ['2CH', { commonName: '2 Crônicas' }], + ['EZR', { commonName: 'Esdras' }], + ['NEH', { commonName: 'Neemias' }], + ['EST', { commonName: 'Ester' }], + ['JOB', { commonName: 'Jó' }], + ['PSA', { commonName: 'Salmos' }], + ['PRO', { commonName: 'Provérbios' }], + ['ECC', { commonName: 'Eclesiastes' }], + ['SNG', { commonName: 'Cânticos' }], + ['ISA', { commonName: 'Isaías' }], + ['JER', { commonName: 'Jeremias' }], + ['LAM', { commonName: 'Lamentações' }], + ['EZK', { commonName: 'Ezequiel' }], + ['DAN', { commonName: 'Daniel' }], + ['HOS', { commonName: 'Oséias' }], + ['JOL', { commonName: 'Joel' }], + ['AMO', { commonName: 'Amós' }], + ['OBA', { commonName: 'Obadias' }], + ['JON', { commonName: 'Jonas' }], + ['MIC', { commonName: 'Miquéias' }], + ['NAM', { commonName: 'Naum' }], + ['HAB', { commonName: 'Habacuque' }], + ['ZEP', { commonName: 'Sofonias' }], + ['HAG', { commonName: 'Ageu' }], + ['ZEC', { commonName: 'Zacarias' }], + ['MAL', { commonName: 'Malaquias' }], + ['MAT', { commonName: 'Mateus' }], + ['MRK', { commonName: 'Marcos' }], + ['LUK', { commonName: 'Lucas' }], + ['JHN', { commonName: 'João' }], + ['ACT', { commonName: 'Atos' }], + ['ROM', { commonName: 'Romanos' }], + ['1CO', { commonName: '1 Coríntios' }], + ['2CO', { commonName: '2 Coríntios' }], + ['GAL', { commonName: 'Gálatas' }], + ['EPH', { commonName: 'Efésios' }], + ['PHP', { commonName: 'Filipenses' }], + ['COL', { commonName: 'Colossenses' }], + ['1TH', { commonName: '1 Tessalonicenses' }], + ['2TH', { commonName: '2 Tessalonicenses' }], + ['1TI', { commonName: '1 Timóteo' }], + ['2TI', { commonName: '2 Timóteo' }], + ['TIT', { commonName: 'Tito' }], + ['PHM', { commonName: 'Filemom' }], + ['HEB', { commonName: 'Hebreus' }], + ['JAS', { commonName: 'Tiago' }], + ['1PE', { commonName: '1 Pedro' }], + ['2PE', { commonName: '2 Pedro' }], + ['1JN', { commonName: '1 João' }], + ['2JN', { commonName: '2 João' }], + ['3JN', { commonName: '3 João' }], + ['JUD', { commonName: 'Judas' }], + ['REV', { commonName: 'Apocalipse' }], +]); + +/** + * Russian book names + */ +export const russianBookMap: BookNameMap = new Map([ + ['GEN', { commonName: 'Бытие' }], + ['EXO', { commonName: 'Исход' }], + ['LEV', { commonName: 'Левит' }], + ['NUM', { commonName: 'Числа' }], + ['DEU', { commonName: 'Второзаконие' }], + ['JOS', { commonName: 'Иисус Навин' }], + ['JDG', { commonName: 'Судьи' }], + ['RUT', { commonName: 'Руфь' }], + ['1SA', { commonName: '1 Царств' }], + ['2SA', { commonName: '2 Царств' }], + ['1KI', { commonName: '3 Царств' }], + ['2KI', { commonName: '4 Царств' }], + ['1CH', { commonName: '1 Паралипоменон' }], + ['2CH', { commonName: '2 Паралипоменон' }], + ['EZR', { commonName: 'Ездра' }], + ['NEH', { commonName: 'Неемия' }], + ['EST', { commonName: 'Есфирь' }], + ['JOB', { commonName: 'Иов' }], + ['PSA', { commonName: 'Псалтирь' }], + ['PRO', { commonName: 'Притчи' }], + ['ECC', { commonName: 'Екклесиаст' }], + ['SNG', { commonName: 'Песнь Песней' }], + ['ISA', { commonName: 'Исаия' }], + ['JER', { commonName: 'Иеремия' }], + ['LAM', { commonName: 'Плач Иеремии' }], + ['EZK', { commonName: 'Иезекииль' }], + ['DAN', { commonName: 'Даниил' }], + ['HOS', { commonName: 'Осия' }], + ['JOL', { commonName: 'Иоиль' }], + ['AMO', { commonName: 'Амос' }], + ['OBA', { commonName: 'Авдий' }], + ['JON', { commonName: 'Иона' }], + ['MIC', { commonName: 'Михей' }], + ['NAM', { commonName: 'Наум' }], + ['HAB', { commonName: 'Аввакум' }], + ['ZEP', { commonName: 'Софония' }], + ['HAG', { commonName: 'Аггей' }], + ['ZEC', { commonName: 'Захария' }], + ['MAL', { commonName: 'Малахия' }], + ['MAT', { commonName: 'Матфея' }], + ['MRK', { commonName: 'Марка' }], + ['LUK', { commonName: 'Луки' }], + ['JHN', { commonName: 'Иоанна' }], + ['ACT', { commonName: 'Деяния' }], + ['ROM', { commonName: 'Римлянам' }], + ['1CO', { commonName: '1 Коринфянам' }], + ['2CO', { commonName: '2 Коринфянам' }], + ['GAL', { commonName: 'Галатам' }], + ['EPH', { commonName: 'Ефесянам' }], + ['PHP', { commonName: 'Филиппийцам' }], + ['COL', { commonName: 'Колоссянам' }], + ['1TH', { commonName: '1 Фессалоникийцам' }], + ['2TH', { commonName: '2 Фессалоникийцам' }], + ['1TI', { commonName: '1 Тимофею' }], + ['2TI', { commonName: '2 Тимофею' }], + ['TIT', { commonName: 'Титу' }], + ['PHM', { commonName: 'Филимону' }], + ['HEB', { commonName: 'Евреям' }], + ['JAS', { commonName: 'Иакова' }], + ['1PE', { commonName: '1 Петра' }], + ['2PE', { commonName: '2 Петра' }], + ['1JN', { commonName: '1 Иоанна' }], + ['2JN', { commonName: '2 Иоанна' }], + ['3JN', { commonName: '3 Иоанна' }], + ['JUD', { commonName: 'Иуды' }], + ['REV', { commonName: 'Откровение' }], +]); + +/** + * Chinese Simplified book names + */ +export const chineseSimplifiedBookMap: BookNameMap = new Map([ + ['GEN', { commonName: '创世记' }], + ['EXO', { commonName: '出埃及记' }], + ['LEV', { commonName: '利未记' }], + ['NUM', { commonName: '民数记' }], + ['DEU', { commonName: '申命记' }], + ['JOS', { commonName: '约书亚记' }], + ['JDG', { commonName: '士师记' }], + ['RUT', { commonName: '路得记' }], + ['1SA', { commonName: '撒母耳记上' }], + ['2SA', { commonName: '撒母耳记下' }], + ['1KI', { commonName: '列王纪上' }], + ['2KI', { commonName: '列王纪下' }], + ['1CH', { commonName: '历代志上' }], + ['2CH', { commonName: '历代志下' }], + ['EZR', { commonName: '以斯拉记' }], + ['NEH', { commonName: '尼希米记' }], + ['EST', { commonName: '以斯帖记' }], + ['JOB', { commonName: '约伯记' }], + ['PSA', { commonName: '诗篇' }], + ['PRO', { commonName: '箴言' }], + ['ECC', { commonName: '传道书' }], + ['SNG', { commonName: '雅歌' }], + ['ISA', { commonName: '以赛亚书' }], + ['JER', { commonName: '耶利米书' }], + ['LAM', { commonName: '耶利米哀歌' }], + ['EZK', { commonName: '以西结书' }], + ['DAN', { commonName: '但以理书' }], + ['HOS', { commonName: '何西阿书' }], + ['JOL', { commonName: '约珥书' }], + ['AMO', { commonName: '阿摩司书' }], + ['OBA', { commonName: '俄巴底亚书' }], + ['JON', { commonName: '约拿书' }], + ['MIC', { commonName: '弥迦书' }], + ['NAM', { commonName: '那鸿书' }], + ['HAB', { commonName: '哈巴谷书' }], + ['ZEP', { commonName: '西番雅书' }], + ['HAG', { commonName: '哈该书' }], + ['ZEC', { commonName: '撒迦利亚书' }], + ['MAL', { commonName: '玛拉基书' }], + ['MAT', { commonName: '马太福音' }], + ['MRK', { commonName: '马可福音' }], + ['LUK', { commonName: '路加福音' }], + ['JHN', { commonName: '约翰福音' }], + ['ACT', { commonName: '使徒行传' }], + ['ROM', { commonName: '罗马书' }], + ['1CO', { commonName: '哥林多前书' }], + ['2CO', { commonName: '哥林多后书' }], + ['GAL', { commonName: '加拉太书' }], + ['EPH', { commonName: '以弗所书' }], + ['PHP', { commonName: '腓立比书' }], + ['COL', { commonName: '歌罗西书' }], + ['1TH', { commonName: '帖撒罗尼迦前书' }], + ['2TH', { commonName: '帖撒罗尼迦后书' }], + ['1TI', { commonName: '提摩太前书' }], + ['2TI', { commonName: '提摩太后书' }], + ['TIT', { commonName: '提多书' }], + ['PHM', { commonName: '腓利门书' }], + ['HEB', { commonName: '希伯来书' }], + ['JAS', { commonName: '雅各书' }], + ['1PE', { commonName: '彼得前书' }], + ['2PE', { commonName: '彼得后书' }], + ['1JN', { commonName: '约翰一书' }], + ['2JN', { commonName: '约翰二书' }], + ['3JN', { commonName: '约翰三书' }], + ['JUD', { commonName: '犹大书' }], + ['REV', { commonName: '启示录' }], +]); + +/** + * Italian book names + */ +export const italianBookMap: BookNameMap = new Map([ + ['GEN', { commonName: 'Genesi' }], + ['EXO', { commonName: 'Esodo' }], + ['LEV', { commonName: 'Levitico' }], + ['NUM', { commonName: 'Numeri' }], + ['DEU', { commonName: 'Deuteronomio' }], + ['JOS', { commonName: 'Giosuè' }], + ['JDG', { commonName: 'Giudici' }], + ['RUT', { commonName: 'Rut' }], + ['1SA', { commonName: '1 Samuele' }], + ['2SA', { commonName: '2 Samuele' }], + ['1KI', { commonName: '1 Re' }], + ['2KI', { commonName: '2 Re' }], + ['1CH', { commonName: '1 Cronache' }], + ['2CH', { commonName: '2 Cronache' }], + ['EZR', { commonName: 'Esdra' }], + ['NEH', { commonName: 'Neemia' }], + ['EST', { commonName: 'Ester' }], + ['JOB', { commonName: 'Giobbe' }], + ['PSA', { commonName: 'Salmi' }], + ['PRO', { commonName: 'Proverbi' }], + ['ECC', { commonName: 'Ecclesiaste' }], + ['SNG', { commonName: 'Cantico dei Cantici' }], + ['ISA', { commonName: 'Isaia' }], + ['JER', { commonName: 'Geremia' }], + ['LAM', { commonName: 'Lamentazioni' }], + ['EZK', { commonName: 'Ezechiele' }], + ['DAN', { commonName: 'Daniele' }], + ['HOS', { commonName: 'Osea' }], + ['JOL', { commonName: 'Gioele' }], + ['AMO', { commonName: 'Amos' }], + ['OBA', { commonName: 'Abdia' }], + ['JON', { commonName: 'Giona' }], + ['MIC', { commonName: 'Michea' }], + ['NAM', { commonName: 'Naum' }], + ['HAB', { commonName: 'Abacuc' }], + ['ZEP', { commonName: 'Sofonia' }], + ['HAG', { commonName: 'Aggeo' }], + ['ZEC', { commonName: 'Zaccaria' }], + ['MAL', { commonName: 'Malachia' }], + ['MAT', { commonName: 'Matteo' }], + ['MRK', { commonName: 'Marco' }], + ['LUK', { commonName: 'Luca' }], + ['JHN', { commonName: 'Giovanni' }], + ['ACT', { commonName: 'Atti' }], + ['ROM', { commonName: 'Romani' }], + ['1CO', { commonName: '1 Corinzi' }], + ['2CO', { commonName: '2 Corinzi' }], + ['GAL', { commonName: 'Galati' }], + ['EPH', { commonName: 'Efesini' }], + ['PHP', { commonName: 'Filippesi' }], + ['COL', { commonName: 'Colossesi' }], + ['1TH', { commonName: '1 Tessalonicesi' }], + ['2TH', { commonName: '2 Tessalonicesi' }], + ['1TI', { commonName: '1 Timoteo' }], + ['2TI', { commonName: '2 Timoteo' }], + ['TIT', { commonName: 'Tito' }], + ['PHM', { commonName: 'Filemone' }], + ['HEB', { commonName: 'Ebrei' }], + ['JAS', { commonName: 'Giacomo' }], + ['1PE', { commonName: '1 Pietro' }], + ['2PE', { commonName: '2 Pietro' }], + ['1JN', { commonName: '1 Giovanni' }], + ['2JN', { commonName: '2 Giovanni' }], + ['3JN', { commonName: '3 Giovanni' }], + ['JUD', { commonName: 'Giuda' }], + ['REV', { commonName: 'Apocalisse' }], +]); + +/** + * Polish book names + */ +export const polishBookMap: BookNameMap = new Map([ + ['GEN', { commonName: 'Rodzaju' }], + ['EXO', { commonName: 'Wyjścia' }], + ['LEV', { commonName: 'Kapłańska' }], + ['NUM', { commonName: 'Liczb' }], + ['DEU', { commonName: 'Powtórzonego Prawa' }], + ['JOS', { commonName: 'Jozuego' }], + ['JDG', { commonName: 'Sędziów' }], + ['RUT', { commonName: 'Rut' }], + ['1SA', { commonName: '1 Samuela' }], + ['2SA', { commonName: '2 Samuela' }], + ['1KI', { commonName: '1 Królewska' }], + ['2KI', { commonName: '2 Królewska' }], + ['1CH', { commonName: '1 Kronik' }], + ['2CH', { commonName: '2 Kronik' }], + ['EZR', { commonName: 'Ezdrasza' }], + ['NEH', { commonName: 'Nehemiasza' }], + ['EST', { commonName: 'Estery' }], + ['JOB', { commonName: 'Hioba' }], + ['PSA', { commonName: 'Psalmów' }], + ['PRO', { commonName: 'Przysłów' }], + ['ECC', { commonName: 'Kaznodziei' }], + ['SNG', { commonName: 'Pieśń nad Pieśniami' }], + ['ISA', { commonName: 'Izajasza' }], + ['JER', { commonName: 'Jeremiasza' }], + ['LAM', { commonName: 'Lamentacje' }], + ['EZK', { commonName: 'Ezechiela' }], + ['DAN', { commonName: 'Daniela' }], + ['HOS', { commonName: 'Ozeasza' }], + ['JOL', { commonName: 'Joela' }], + ['AMO', { commonName: 'Amosa' }], + ['OBA', { commonName: 'Abdiasza' }], + ['JON', { commonName: 'Jonasza' }], + ['MIC', { commonName: 'Micheasza' }], + ['NAM', { commonName: 'Nahuma' }], + ['HAB', { commonName: 'Habakuka' }], + ['ZEP', { commonName: 'Sofoniasza' }], + ['HAG', { commonName: 'Aggeusza' }], + ['ZEC', { commonName: 'Zachariasza' }], + ['MAL', { commonName: 'Malachiasza' }], + ['MAT', { commonName: 'Mateusza' }], + ['MRK', { commonName: 'Marka' }], + ['LUK', { commonName: 'Łukasza' }], + ['JHN', { commonName: 'Jana' }], + ['ACT', { commonName: 'Dzieje Apostolskie' }], + ['ROM', { commonName: 'Rzymian' }], + ['1CO', { commonName: '1 Koryntian' }], + ['2CO', { commonName: '2 Koryntian' }], + ['GAL', { commonName: 'Galatów' }], + ['EPH', { commonName: 'Efezjan' }], + ['PHP', { commonName: 'Filipian' }], + ['COL', { commonName: 'Kolosan' }], + ['1TH', { commonName: '1 Tesaloniczan' }], + ['2TH', { commonName: '2 Tesaloniczan' }], + ['1TI', { commonName: '1 Tymoteusza' }], + ['2TI', { commonName: '2 Tymoteusza' }], + ['TIT', { commonName: 'Tytusa' }], + ['PHM', { commonName: 'Filemona' }], + ['HEB', { commonName: 'Hebrajczyków' }], + ['JAS', { commonName: 'Jakuba' }], + ['1PE', { commonName: '1 Piotra' }], + ['2PE', { commonName: '2 Piotra' }], + ['1JN', { commonName: '1 Jana' }], + ['2JN', { commonName: '2 Jana' }], + ['3JN', { commonName: '3 Jana' }], + ['JUD', { commonName: 'Judy' }], + ['REV', { commonName: 'Objawienie' }], +]); + +/** + * Dutch book names + */ +export const dutchBookMap: BookNameMap = new Map([ + ['GEN', { commonName: 'Genesis' }], + ['EXO', { commonName: 'Exodus' }], + ['LEV', { commonName: 'Leviticus' }], + ['NUM', { commonName: 'Numeri' }], + ['DEU', { commonName: 'Deuteronomium' }], + ['JOS', { commonName: 'Jozua' }], + ['JDG', { commonName: 'Richteren' }], + ['RUT', { commonName: 'Ruth' }], + ['1SA', { commonName: '1 Samuël' }], + ['2SA', { commonName: '2 Samuël' }], + ['1KI', { commonName: '1 Koningen' }], + ['2KI', { commonName: '2 Koningen' }], + ['1CH', { commonName: '1 Kronieken' }], + ['2CH', { commonName: '2 Kronieken' }], + ['EZR', { commonName: 'Ezra' }], + ['NEH', { commonName: 'Nehemia' }], + ['EST', { commonName: 'Esther' }], + ['JOB', { commonName: 'Job' }], + ['PSA', { commonName: 'Psalmen' }], + ['PRO', { commonName: 'Spreuken' }], + ['ECC', { commonName: 'Prediker' }], + ['SNG', { commonName: 'Hooglied' }], + ['ISA', { commonName: 'Jesaja' }], + ['JER', { commonName: 'Jeremia' }], + ['LAM', { commonName: 'Klaagliederen' }], + ['EZK', { commonName: 'Ezechiël' }], + ['DAN', { commonName: 'Daniël' }], + ['HOS', { commonName: 'Hosea' }], + ['JOL', { commonName: 'Joël' }], + ['AMO', { commonName: 'Amos' }], + ['OBA', { commonName: 'Obadja' }], + ['JON', { commonName: 'Jona' }], + ['MIC', { commonName: 'Micha' }], + ['NAM', { commonName: 'Nahum' }], + ['HAB', { commonName: 'Habakuk' }], + ['ZEP', { commonName: 'Sefanja' }], + ['HAG', { commonName: 'Haggaï' }], + ['ZEC', { commonName: 'Zacharia' }], + ['MAL', { commonName: 'Maleachi' }], + ['MAT', { commonName: 'Mattheüs' }], + ['MRK', { commonName: 'Marcus' }], + ['LUK', { commonName: 'Lucas' }], + ['JHN', { commonName: 'Johannes' }], + ['ACT', { commonName: 'Handelingen' }], + ['ROM', { commonName: 'Romeinen' }], + ['1CO', { commonName: '1 Korintiërs' }], + ['2CO', { commonName: '2 Korintiërs' }], + ['GAL', { commonName: 'Galaten' }], + ['EPH', { commonName: 'Efeziërs' }], + ['PHP', { commonName: 'Filippenzen' }], + ['COL', { commonName: 'Kolossenzen' }], + ['1TH', { commonName: '1 Tessalonicenzen' }], + ['2TH', { commonName: '2 Tessalonicenzen' }], + ['1TI', { commonName: '1 Timoteüs' }], + ['2TI', { commonName: '2 Timoteüs' }], + ['TIT', { commonName: 'Titus' }], + ['PHM', { commonName: 'Filemon' }], + ['HEB', { commonName: 'Hebreeën' }], + ['JAS', { commonName: 'Jakobus' }], + ['1PE', { commonName: '1 Petrus' }], + ['2PE', { commonName: '2 Petrus' }], + ['1JN', { commonName: '1 Johannes' }], + ['2JN', { commonName: '2 Johannes' }], + ['3JN', { commonName: '3 Johannes' }], + ['JUD', { commonName: 'Judas' }], + ['REV', { commonName: 'Openbaring' }], +]); + +/** + * Map of language codes to book name maps. + * This extends the existing bookIdMap in book-order.ts + */ +export const additionalBookIdMap = new Map([ + // Romanian + ['ro', romanianBookMap], + ['ron', romanianBookMap], + ['rum', romanianBookMap], + + // Spanish + ['es', spanishBookMap], + ['spa', spanishBookMap], + + // French + ['fr', frenchBookMap], + ['fra', frenchBookMap], + ['fre', frenchBookMap], + + // German + ['de', germanBookMap], + ['deu', germanBookMap], + ['ger', germanBookMap], + + // Portuguese + ['pt', portugueseBookMap], + ['por', portugueseBookMap], + + // Russian + ['ru', russianBookMap], + ['rus', russianBookMap], + + // Chinese Simplified + ['zh', chineseSimplifiedBookMap], + ['zho', chineseSimplifiedBookMap], + ['chi', chineseSimplifiedBookMap], + ['cmn', chineseSimplifiedBookMap], + + // Italian + ['it', italianBookMap], + ['ita', italianBookMap], + + // Polish + ['pl', polishBookMap], + ['pol', polishBookMap], + + // Dutch + ['nl', dutchBookMap], + ['nld', dutchBookMap], + ['dut', dutchBookMap], +]); + +/** + * Gets a book name map for the specified language code. + * Falls back to null if the language is not found. + */ +export function getBookNameMap(languageCode: string): BookNameMap | null { + return additionalBookIdMap.get(languageCode.toLowerCase()) ?? null; +} + +/** + * Gets a book name for the specified language and book ID. + * Falls back to null if not found. + */ +export function getBookName( + languageCode: string, + bookId: string +): string | null { + const map = getBookNameMap(languageCode); + if (!map) return null; + return map.get(bookId)?.commonName ?? null; +} diff --git a/packages/helloao-tools/generation/book-order.ts b/packages/helloao-tools/generation/book-order.ts index aff189e..f89903a 100644 --- a/packages/helloao-tools/generation/book-order.ts +++ b/packages/helloao-tools/generation/book-order.ts @@ -1,3 +1,5 @@ +import { additionalBookIdMap } from './book-names-data.js'; + /** * Defines a map that maps the book ID of apocryphal books to the numerical order of the book. */ @@ -331,10 +333,37 @@ const arabicBookMap = new Map([ /** * Defines a map that maps a locale ID to a book name map. + * This includes English and Arabic (defined here) plus additional languages + * from book-names-data.ts (Romanian, Spanish, French, German, Portuguese, + * Russian, Chinese, Italian, Polish, Dutch). */ export const bookIdMap = new Map([ ['en', englishBookMap], ['en-US', englishBookMap], ['eng', englishBookMap], ['arb', arabicBookMap], + // Merge with additional languages from book-names-data.ts + ...additionalBookIdMap, ]); + +/** + * Reverse mapping from book number to book ID. + * Used for formats like Beblia XML that use book numbers (1-66) instead of IDs (GEN, EXO, etc.) + * + * @example + * bookNumberToIdMap.get(1) // 'GEN' + * bookNumberToIdMap.get(40) // 'MAT' + */ +export const bookNumberToIdMap = new Map( + [...bookOrderMap].map(([id, num]) => [num, id]) +); + +/** + * Gets the book ID for a given book number. + * + * @param bookNumber - The book number (1-66 for standard canon, higher for apocrypha) + * @returns The book ID (e.g., 'GEN', 'EXO') or undefined if not found + */ +export function getBookIdFromNumber(bookNumber: number): string | undefined { + return bookNumberToIdMap.get(bookNumber); +} diff --git a/packages/helloao-tools/generation/common-types.ts b/packages/helloao-tools/generation/common-types.ts index c5b8442..5f29bab 100644 --- a/packages/helloao-tools/generation/common-types.ts +++ b/packages/helloao-tools/generation/common-types.ts @@ -16,7 +16,7 @@ export type InputFileMetadata = | InputCommentaryMetadata; export interface InputTranslationFile extends InputFileBase { - fileType: 'usfm' | 'usx' | 'json'; + fileType: 'usfm' | 'usx' | 'json' | 'beblia-xml'; metadata: InputTranslationMetadata; } diff --git a/packages/helloao-tools/generation/dataset.ts b/packages/helloao-tools/generation/dataset.ts index 5d67a06..215e7d7 100644 --- a/packages/helloao-tools/generation/dataset.ts +++ b/packages/helloao-tools/generation/dataset.ts @@ -1,5 +1,6 @@ import { UsfmParser } from '../parser/usfm-parser.js'; import { USXParser } from '../parser/usx-parser.js'; +import { BebliaXmlParser } from '../parser/beblia-xml-parser.js'; import { Commentary, CommentaryBook, @@ -139,6 +140,7 @@ export function generateDataset( let usfmParser = new UsfmParser(); let usxParser = new USXParser(parser); let codexParser = new CodexParser(); + let bebliaXmlParser = new BebliaXmlParser(parser); let csvCommentaryParser = new CommentaryCsvParser(); let tyndaleXmlParser = new TyndaleXmlParser(parser); @@ -152,6 +154,7 @@ export function generateDataset( | UsfmParser | USXParser | CodexParser + | BebliaXmlParser | CommentaryCsvParser | TyndaleXmlParser; if (file.fileType === 'usfm') { @@ -160,6 +163,24 @@ export function generateDataset( parser = usxParser; } else if (file.fileType === 'json') { parser = codexParser; + } else if (file.fileType === 'beblia-xml') { + // Beblia XML files contain all books in one file + // Parse all books and add each one + const allBooks = bebliaXmlParser.parseAllBooks(file.content); + for (const parsed of allBooks) { + if ( + 'parseMessages' in parsed && + parsed.parseMessages && + file.name + ) { + const messages = (output.parseMessages = + output.parseMessages || {}); + const key = `${file.name}:${parsed.id}`; + messages[key] = parsed.parseMessages; + } + addTranslationTree(file as InputTranslationFile, parsed); + } + continue; // Skip the normal parsing flow } else if (file.fileType === 'commentary/csv') { parser = csvCommentaryParser; } else if (file.fileType === 'commentary/tyndale-xml') { diff --git a/packages/helloao-tools/parser/beblia-xml-parser.ts b/packages/helloao-tools/parser/beblia-xml-parser.ts new file mode 100644 index 0000000..bd5d5d9 --- /dev/null +++ b/packages/helloao-tools/parser/beblia-xml-parser.ts @@ -0,0 +1,297 @@ +import { Chapter, ParseTree, Verse, ParseMessage } from './types.js'; +import { getBookIdFromNumber } from '../generation/book-order.js'; + +/** + * The version of the Beblia XML parser. + * Used to determine whether input files need to be re-parsed. + */ +export const BEBLIA_PARSER_VERSION = '1'; + +/** + * Metadata extracted from the Beblia XML root element. + */ +export interface BebliaMetadata { + translation: string; + version?: string; + link?: string; + status?: string; +} + +/** + * Defines a class that is able to parse Beblia XML content. + * + * Beblia XML format: + * ```xml + * + * + * + * + * + * In the beginning... + * + * + * + * + * ``` + */ +export class BebliaXmlParser { + private _domParser: DOMParser; + private _messages: ParseMessage[] = []; + + constructor(domParser: DOMParser) { + this._domParser = domParser; + } + + /** + * Parses the specified Beblia XML content. + * + * @param xml - The Beblia XML content to parse. + * @param bookNumber - Optional book number to extract. If not specified, returns the first book. + * @returns The parse tree that was generated. + */ + public parse(xml: string, bookNumber?: number): ParseTree { + this._messages = []; + const doc = this._domParser.parseFromString(xml, 'application/xml'); + const bibleElement = doc.documentElement; + + // Check for XML parsing errors + const parseError = bibleElement.querySelector('parsererror'); + if (parseError) { + throw new Error( + `Failed to parse Beblia XML: ${parseError.textContent}` + ); + } + + // Extract metadata from root element + const metadata = this.extractMetadata(bibleElement); + + // Find the book to parse + const bookElement = bookNumber + ? bibleElement.querySelector(`book[number="${bookNumber}"]`) + : bibleElement.querySelector('book'); + + if (!bookElement) { + throw new Error( + bookNumber + ? `Book number ${bookNumber} not found in Beblia XML` + : 'No books found in Beblia XML' + ); + } + + const bookNum = parseInt( + bookElement.getAttribute('number') || '0', + 10 + ); + const bookId = getBookIdFromNumber(bookNum); + + if (!bookId) { + this._messages.push({ + type: 'warning', + message: `Unknown book number: ${bookNum}`, + }); + } + + const root: ParseTree = { + type: 'root', + id: bookId, + content: [], + }; + + // Parse chapters + const chapters = bookElement.querySelectorAll('chapter'); + for (const chapterElement of chapters) { + const chapter = this.parseChapter(chapterElement); + if (chapter) { + root.content.push(chapter); + } + } + + if (this._messages.length > 0) { + root.parseMessages = this._messages.slice(); + } + + return root; + } + + /** + * Parses all books in the XML file. + * Returns an array of ParseTrees, one for each book. + * + * @param xml - The Beblia XML content to parse. + * @returns Array of parse trees, one per book. + */ + public parseAllBooks(xml: string): ParseTree[] { + this._messages = []; + const doc = this._domParser.parseFromString(xml, 'application/xml'); + const bibleElement = doc.documentElement; + + // Check for XML parsing errors + const parseError = bibleElement.querySelector('parsererror'); + if (parseError) { + throw new Error( + `Failed to parse Beblia XML: ${parseError.textContent}` + ); + } + + const books = bibleElement.querySelectorAll('book'); + const results: ParseTree[] = []; + + for (const bookElement of books) { + const bookNum = parseInt( + bookElement.getAttribute('number') || '0', + 10 + ); + const bookId = getBookIdFromNumber(bookNum); + + if (!bookId) { + this._messages.push({ + type: 'warning', + message: `Unknown book number: ${bookNum}, skipping`, + }); + continue; + } + + const root: ParseTree = { + type: 'root', + id: bookId, + content: [], + }; + + // Parse chapters + const chapters = bookElement.querySelectorAll('chapter'); + for (const chapterElement of chapters) { + const chapter = this.parseChapter(chapterElement); + if (chapter) { + root.content.push(chapter); + } + } + + if (this._messages.length > 0) { + root.parseMessages = this._messages.slice(); + this._messages = []; // Reset for next book + } + + results.push(root); + } + + return results; + } + + /** + * Extracts metadata from the root bible element. + */ + public extractMetadata(bibleElement: Element): BebliaMetadata { + return { + translation: bibleElement.getAttribute('translation') || '', + version: bibleElement.getAttribute('version') || undefined, + link: bibleElement.getAttribute('link') || undefined, + status: bibleElement.getAttribute('status') || undefined, + }; + } + + /** + * Parses metadata from raw XML string without full parsing. + */ + public parseMetadataOnly(xml: string): BebliaMetadata { + const doc = this._domParser.parseFromString(xml, 'application/xml'); + return this.extractMetadata(doc.documentElement); + } + + /** + * Gets the list of book numbers available in the XML. + */ + public getAvailableBooks(xml: string): number[] { + const doc = this._domParser.parseFromString(xml, 'application/xml'); + const books = doc.documentElement.querySelectorAll('book'); + const bookNumbers: number[] = []; + + for (const book of books) { + const num = parseInt(book.getAttribute('number') || '0', 10); + if (num > 0) { + bookNumbers.push(num); + } + } + + return bookNumbers.sort((a, b) => a - b); + } + + /** + * Parses a chapter element. + */ + private parseChapter(chapterElement: Element): Chapter | null { + const chapterNum = parseInt( + chapterElement.getAttribute('number') || '0', + 10 + ); + + if (chapterNum <= 0) { + this._messages.push({ + type: 'warning', + message: `Invalid chapter number: ${chapterElement.getAttribute('number')}`, + }); + return null; + } + + const chapter: Chapter = { + type: 'chapter', + number: chapterNum, + content: [], + footnotes: [], + }; + + const verses = chapterElement.querySelectorAll('verse'); + for (const verseElement of verses) { + const verse = this.parseVerse(verseElement); + if (verse) { + chapter.content.push(verse); + } + } + + return chapter; + } + + /** + * Parses a verse element. + */ + private parseVerse(verseElement: Element): Verse | null { + const verseNum = parseInt( + verseElement.getAttribute('number') || '0', + 10 + ); + + if (verseNum <= 0) { + this._messages.push({ + type: 'warning', + message: `Invalid verse number: ${verseElement.getAttribute('number')}`, + }); + return null; + } + + const text = verseElement.textContent?.trim() || ''; + + const verse: Verse = { + type: 'verse', + number: verseNum, + content: text ? [text] : [], + }; + + return verse; + } +} + +/** + * Checks if an XML string is in Beblia format. + * + * @param xml - The XML content to check. + * @returns true if the XML appears to be in Beblia format. + */ +export function isBebliaXml(xml: string): boolean { + // Quick check without full parsing + return ( + xml.includes('