← back to Dear Bubbe Nextjs
lib/yiddish-dictionary/dictionary-api.ts
342 lines
/**
* Yiddish Dictionary API for Dear Bubbe
* Provides lookup and management functions for the Yiddish dictionary database
*/
import Database from 'better-sqlite3';
import path from 'path';
export interface DictionaryEntry {
id: number;
yiddish: string;
yiddish_latin?: string;
english: string;
pos?: string;
gender?: string;
plural?: string;
tags?: string;
usage_example?: string;
cultural_note?: string;
source_name?: string;
bubbe_approved: number;
}
export interface Phrase {
id: number;
yiddish: string;
yiddish_latin?: string;
english: string;
literal_meaning?: string;
usage_context?: string;
cultural_note?: string;
bubbe_approved: number;
}
export interface BubbeFavorite {
id: number;
yiddish: string;
english: string;
context?: string;
intensity?: number;
usage_example?: string;
}
class YiddishDictionary {
private db: Database.Database;
private readonly dbPath: string;
constructor(dbPath?: string) {
this.dbPath = dbPath || path.join(__dirname, 'yiddish_dictionary.sqlite');
this.db = new Database(this.dbPath, { readonly: false });
// Enable foreign keys
this.db.exec('PRAGMA foreign_keys = ON');
}
/**
* Search for entries by English word
*/
searchEnglish(query: string, limit: number = 10): DictionaryEntry[] {
const stmt = this.db.prepare(`
SELECT
e.*,
s.name as source_name
FROM entries e
LEFT JOIN sources s ON e.source_id = s.id
WHERE e.english LIKE ? AND e.bubbe_approved = 1
ORDER BY
CASE WHEN e.english = ? THEN 1
WHEN e.english LIKE ? THEN 2
ELSE 3 END,
LENGTH(e.english)
LIMIT ?
`);
return stmt.all(`%${query}%`, query, `${query}%`, limit) as DictionaryEntry[];
}
/**
* Search for entries by Yiddish word (supports both Hebrew script and transliteration)
*/
searchYiddish(query: string, limit: number = 10): DictionaryEntry[] {
const stmt = this.db.prepare(`
SELECT
e.*,
s.name as source_name
FROM entries e
LEFT JOIN sources s ON e.source_id = s.id
WHERE (e.yiddish LIKE ? OR e.yiddish_latin LIKE ?)
AND e.bubbe_approved = 1
ORDER BY
CASE WHEN e.yiddish = ? OR e.yiddish_latin = ? THEN 1
WHEN e.yiddish LIKE ? OR e.yiddish_latin LIKE ? THEN 2
ELSE 3 END,
LENGTH(e.yiddish)
LIMIT ?
`);
return stmt.all(
`%${query}%`, `%${query}%`,
query, query,
`${query}%`, `${query}%`,
limit
) as DictionaryEntry[];
}
/**
* Get a random insult/scolding term for Bubbe to use
*/
getRandomInsult(): DictionaryEntry | null {
const stmt = this.db.prepare(`
SELECT e.* FROM entries e
WHERE e.bubbe_approved = 1
AND (e.tags LIKE '%insult%'
OR e.tags LIKE '%scold%'
OR e.pos = 'interjection'
OR e.english LIKE '%fool%'
OR e.english LIKE '%stupid%'
OR e.english LIKE '%idiot%')
ORDER BY RANDOM()
LIMIT 1
`);
return stmt.get() as DictionaryEntry | null;
}
/**
* Get a random endearment term (used sarcastically by Bubbe)
*/
getRandomEndearment(): DictionaryEntry | null {
const stmt = this.db.prepare(`
SELECT e.* FROM entries e
WHERE e.bubbe_approved = 1
AND (e.tags LIKE '%endearment%'
OR e.tags LIKE '%affection%'
OR e.english LIKE '%dear%'
OR e.english LIKE '%sweet%')
ORDER BY RANDOM()
LIMIT 1
`);
return stmt.get() as DictionaryEntry | null;
}
/**
* Get phrases for a specific context
*/
getPhrasesByContext(context: string, limit: number = 5): Phrase[] {
const stmt = this.db.prepare(`
SELECT * FROM phrases
WHERE usage_context LIKE ?
AND bubbe_approved = 1
ORDER BY RANDOM()
LIMIT ?
`);
return stmt.all(`%${context}%`, limit) as Phrase[];
}
/**
* Get Bubbe's favorite expressions
*/
getBubbeFavorites(context?: string, minIntensity?: number): BubbeFavorite[] {
let query = 'SELECT * FROM bubbe_favorites WHERE 1=1';
const params: any[] = [];
if (context) {
query += ' AND context = ?';
params.push(context);
}
if (minIntensity !== undefined) {
query += ' AND intensity >= ?';
params.push(minIntensity);
}
query += ' ORDER BY intensity DESC, RANDOM()';
const stmt = this.db.prepare(query);
return stmt.all(...params) as BubbeFavorite[];
}
/**
* Get a random Bubbe expression based on intensity
*/
getRandomExpression(minIntensity: number = 1, maxIntensity: number = 10): BubbeFavorite | null {
const stmt = this.db.prepare(`
SELECT * FROM bubbe_favorites
WHERE intensity BETWEEN ? AND ?
ORDER BY RANDOM()
LIMIT 1
`);
return stmt.get(minIntensity, maxIntensity) as BubbeFavorite | null;
}
/**
* Record usage of a term (for learning patterns)
*/
recordUsage(term: string, context?: string, entryId?: number): void {
const stmt = this.db.prepare(`
INSERT INTO usage_stats (entry_id, term, context, usage_count, last_used)
VALUES (?, ?, ?, 1, CURRENT_TIMESTAMP)
ON CONFLICT (term) DO UPDATE SET
usage_count = usage_count + 1,
last_used = CURRENT_TIMESTAMP
`);
stmt.run(entryId || null, term, context || null);
}
/**
* Get most frequently used terms
*/
getMostUsedTerms(limit: number = 20): any[] {
const stmt = this.db.prepare(`
SELECT
us.term,
us.usage_count,
us.last_used,
e.yiddish,
e.english
FROM usage_stats us
LEFT JOIN entries e ON us.entry_id = e.id
ORDER BY us.usage_count DESC, us.last_used DESC
LIMIT ?
`);
return stmt.all(limit);
}
/**
* Check if a term is banned
*/
isBanned(term: string, language: string = 'en'): boolean {
const stmt = this.db.prepare(`
SELECT COUNT(*) as count FROM banned_terms
WHERE term = ? AND language = ?
`);
const result = stmt.get(term.toLowerCase(), language) as { count: number };
return result.count > 0;
}
/**
* Add a new banned term
*/
addBannedTerm(
term: string,
language: string,
category: string,
severity: string,
reason?: string,
replacement?: string
): void {
const stmt = this.db.prepare(`
INSERT OR IGNORE INTO banned_terms
(term, language, category, severity, reason, replacement)
VALUES (?, ?, ?, ?, ?, ?)
`);
stmt.run(term.toLowerCase(), language, category, severity, reason, replacement);
}
/**
* Get suggestions for Yiddish terms based on context
*/
getSuggestions(context: 'greeting' | 'insult' | 'complaint' | 'advice', limit: number = 5): DictionaryEntry[] {
const contextMap = {
greeting: ['hello', 'good', 'peace', 'welcome'],
insult: ['fool', 'idiot', 'stupid', 'worthless'],
complaint: ['complain', 'whine', 'grumble', 'moan'],
advice: ['should', 'must', 'better', 'wise']
};
const keywords = contextMap[context] || [];
const placeholders = keywords.map(() => '?').join(',');
const stmt = this.db.prepare(`
SELECT DISTINCT e.* FROM entries e
WHERE e.bubbe_approved = 1
AND (
e.tags LIKE '%${context}%'
OR e.english IN (${placeholders})
OR ${keywords.map(() => 'e.english LIKE ?').join(' OR ')}
)
ORDER BY RANDOM()
LIMIT ?
`);
const params = [
...keywords,
...keywords.map(k => `%${k}%`),
limit
];
return stmt.all(...params) as DictionaryEntry[];
}
/**
* Close database connection
*/
close(): void {
this.db.close();
}
/**
* Get database statistics
*/
getStats(): any {
const stats = {
totalEntries: this.db.prepare('SELECT COUNT(*) as count FROM entries').get() as { count: number },
approvedEntries: this.db.prepare('SELECT COUNT(*) as count FROM entries WHERE bubbe_approved = 1').get() as { count: number },
needsReview: this.db.prepare('SELECT COUNT(*) as count FROM entries WHERE bubbe_approved = 0').get() as { count: number },
totalPhrases: this.db.prepare('SELECT COUNT(*) as count FROM phrases').get() as { count: number },
totalFavorites: this.db.prepare('SELECT COUNT(*) as count FROM bubbe_favorites').get() as { count: number },
bannedTerms: this.db.prepare('SELECT COUNT(*) as count FROM banned_terms').get() as { count: number },
sources: this.db.prepare('SELECT COUNT(*) as count FROM sources').get() as { count: number }
};
return {
totalEntries: stats.totalEntries.count,
approvedEntries: stats.approvedEntries.count,
needsReview: stats.needsReview.count,
totalPhrases: stats.totalPhrases.count,
totalFavorites: stats.totalFavorites.count,
bannedTerms: stats.bannedTerms.count,
sources: stats.sources.count
};
}
}
// Singleton instance
let dictionaryInstance: YiddishDictionary | null = null;
export function getDictionary(dbPath?: string): YiddishDictionary {
if (!dictionaryInstance) {
dictionaryInstance = new YiddishDictionary(dbPath);
}
return dictionaryInstance;
}
export default YiddishDictionary;