Cultural Relevance System
Last Updated: November 17, 2025
Status: Production (Phase 6)
Scope: Estonian-first cultural intelligence for gift recommendations
Table of Contents
- Overview
- Architecture
- Cultural Rules Service
- Age-Based Cultural Filtering
- Estonian Cultural Calendar
- Estonian Product Prioritization
- Gender Appropriateness
- Integration Points
- Testing & Validation
- Performance Metrics
- Future Enhancements
Overview
The Cultural Relevance System provides multi-dimensional cultural intelligence specifically optimized for the Estonian market. Unlike generic internationalization approaches that simply translate text, this system deeply understands Estonian culture, customs, and social norms.
Key Capabilities
- Age-appropriate filtering - Prevents culturally inappropriate gifts (e.g., alcohol to children, toys to elderly)
- Estonian occasion awareness - Recognizes 9 Estonian-specific holidays with budget inference
- Local product prioritization - Boosts Estonian authors, publishers, and cultural products
- Gender sensitivity - Adjusts scores for gender-appropriate gifts
- Language-aware processing - Estonian morphology handling and bilingual support
Design Philosophy
Cultural Appropriateness > Raw Relevance Scores
The system enforces cultural rules as hard constraints in the funnel stage (Stage B) before expensive LLM reranking, ensuring:
- No culturally inappropriate products reach the user
- Local cultural products are naturally prioritized
- Estonian customs are respected throughout the pipeline
Architecture
System Flow Overview
Key Stages:
- Language Detection - Determines if user is Estonian, English, or mixed
- Context Extraction - Extracts age, recipient, occasion from query
- Cultural Filtering - Applies age-appropriate hard constraints
- Estonian Boosting - Prioritizes local products for Estonian users
- Final Selection - Culturally relevant products ranked and displayed
Cultural Rules Service
Location: app/api/chat/services/language.ts
Core Service Class
export class LanguageService {
// Language detection
static detectLanguage(message: string): 'et' | 'en' | 'mixed'
// Cultural rules
static getCulturalRules(): CulturalRules
static getEstonianOccasions(): EstonianOccasion[]
static getBudgetHintByOccasion(occasion: string): BudgetRange | null
// Estonian morphology
static toEstonianDative(noun: string): string
static normalizeRecipient(recipient: string): string
static normalizeOccasion(occasion: string): string
// Product filtering & boosting
static boostEstonianProducts(products: Product[], language: string): Product[]
static filterByAge(products: Product[], ageGroup: string): Product[]
}
Cultural Rules Data Structure
export interface CulturalRules {
elderlyForbidden: string[]; // Products inappropriate for 60+ recipients
childForbidden: string[]; // Products inappropriate for 0-12 recipients
teenForbidden: string[]; // Products inappropriate for 13-19 recipients
}
const CULTURAL_RULES: CulturalRules = {
elderlyForbidden: [
'mänguasjad', 'beebi', 'laste', 'nukkude', 'teismeliste',
'gaming', 'videomängud', 'baby', 'toys', 'kids',
'mähkmed', 'diapers'
],
childForbidden: [
'alkohol', 'vein', 'spirits', 'alcohol', 'wine', 'beer', 'õlu'
],
teenForbidden: [
'beebi', 'baby', 'väikelaste', 'toddler', 'mähkmed'
]
};
Age-Based Cultural Filtering
Location: app/api/chat/services/stage-b.ts:395-507
How It Works
Age-based filtering ensures products are culturally appropriate for the recipient's age group.
Implementation Strategy
Age-based filtering operates on three levels:
1. Hard Cultural Rules (Forbidden Categories)
Applied to elderly, child, and teen recipients:
function passesAgeBasedFilters(
candidate: FunnelCandidate,
env: StageBEnvironment,
state: StageBState
): boolean {
const { age, culturalRules, debug } = env;
// ELDERLY FILTER (60+)
if (age.isElderlyRecipient) {
const category = (candidate.csv_category || '').toLowerCase();
const productType = (candidate.product_type || '').toLowerCase();
const title = (candidate.title || '').toLowerCase();
if (
culturalRules.elderlyForbidden.some(
forbidden =>
category.includes(forbidden) ||
productType.includes(forbidden) ||
title.includes(forbidden)
)
) {
state.categoryFiltered++;
return false; // HARD REJECT
}
}
// CHILD FILTER (0-12)
if (age.isChildRecipient) {
const category = (candidate.csv_category || '').toLowerCase();
if (culturalRules.childForbidden.some(forbidden =>
category.includes(forbidden)
)) {
state.categoryFiltered++;
return false; // HARD REJECT
}
}
// TEEN FILTER (13-19)
if (age.isTeenRecipient) {
const category = (candidate.csv_category || '').toLowerCase();
const title = (candidate.title || '').toLowerCase();
if (
culturalRules.teenForbidden.some(
forbidden => category.includes(forbidden) || title.includes(forbidden)
)
) {
state.categoryFiltered++;
return false; // HARD REJECT
}
}
return true; // PASSES AGE FILTER
}
Implementation note: child alcohol filtering currently inspects only
csv_category(not title/product_type). Alcoholic items with neutral categories can slip through.
2. Infant Product Detection
Comprehensive keyword matching for infant-specific products:
const infantKeywords = [
// Estonian terms
'beebi', 'imiku', 'imikut', 'väikelapse', 'väikelast',
'vastsündinu', 'mähkme', 'mähkmed', 'pisipõnn', 'pisipõnnidele',
// English terms
'baby', 'infant', 'toddler', 'newborn',
// Product-specific
'lutipudel', 'kõristi', 'beebitekk', 'beebilapp', 'kaisulapp',
// Age ranges
'vanus 0-3', 'vanus 0-2', 'vanus 0-5',
// Brand-specific (Estonian characters)
'lotte tegelane', // Lotte character (ages 3-6)
'lotte multifilm', // Lotte cartoon
'roosi', // Roosi character (baby sister)
'väike õde' // Little sister (toddler-focused)
];
Logic:
if (age.shouldAvoidInfantProducts && !age.isExplicitBabyGift) {
const haystack = [
candidate.csv_category,
candidate.product_type,
candidate.title
].join(' ').toLowerCase();
if (infantKeywords.some(keyword => haystack.includes(keyword))) {
return false; // REJECT: Infant product for non-infant recipient
}
}
3. Content Difficulty Filter
Location: stage-b.ts:509-567
Prevents advanced/professional content for children:
function passesContentDifficultyFilter(
candidate: FunnelCandidate,
env: StageBEnvironment,
state: StageBState
): boolean {
const age = env.context.recipientAge;
// Only apply to children (age < 13)
if (!age || age >= 13) {
return true;
}
const text = [
candidate.title || '',
candidate.csv_category || '',
candidate.description || ''
].join(' ').toLowerCase();
// Detect advanced/professional content
const ADVANCED_INDICATORS = [
'professional', 'advanced', 'theory', 'technical',
'collector', 'gallery', 'exhibition', 'catalog',
'näitus', 'professionaal', 'teooria', 'tehniline',
'eksklusiivne', 'kollektsionäär'
];
const hasAdvancedContent = ADVANCED_INDICATORS.some(kw => text.includes(kw));
if (hasAdvancedContent) {
return false; // REJECT: Too advanced for child
}
return true;
}
Filter Execution Order
Cultural filters (F2 and F3) ensure age-appropriate products before expensive operations.
Estonian Cultural Calendar
Location: language.ts:77-132
Estonian-Specific Occasions
The system understands 9 major Estonian holidays and automatically infers appropriate gift budgets.
Budget Inference - When these occasions are detected, the system automatically suggests appropriate price ranges:
Estonian-Specific Occasions
The system recognizes 9 major Estonian occasions with culturally-appropriate budget ranges:
const ESTONIAN_OCCASIONS: EstonianOccasion[] = [
{
name: 'Jaanipäev', // MAJOR Estonian holiday
date: '06-23', // Midsummer Day
typicalBudget: { min: 15, max: 40 },
keywords: ['jaanipäev', 'jaani', 'jaanikingitus']
},
{
name: 'Kadripäev', // St. Catherine's Day
date: '11-25',
typicalBudget: { min: 10, max: 25 },
keywords: ['kadripäev', 'kadri', 'kadrikingitus']
},
{
name: 'Mardipäev', // St. Martin's Day
date: '11-10',
typicalBudget: { min: 10, max: 25 },
keywords: ['mardipäev', 'mardi', 'martikingitus']
},
{
name: 'Isadepäev', // Father's Day
date: '11-14', // Different from US (June)
typicalBudget: { min: 20, max: 60 },
keywords: ['isadepäev', 'isa päev', 'isale']
},
{
name: 'Emadepäev', // Mother's Day
date: '05-09', // Fixed date (not floating Sunday)
typicalBudget: { min: 20, max: 60 },
keywords: ['emadepäev', 'ema päev', 'emale']
},
{
name: 'Valentinipäev', // Valentine's Day / Friend's Day
date: '02-14',
typicalBudget: { min: 15, max: 50 },
keywords: ['valentinipäev', 'valentine', 'sõbrapäev']
},
{
name: 'Naistepäev', // Women's Day (ex-Soviet tradition)
date: '03-08', // International Women's Day
typicalBudget: { min: 15, max: 40 },
keywords: ['naistepäev', 'naisele', '8. märts']
},
{
name: 'Jõulud', // Christmas
date: '12-24', // Christmas Eve (main celebration)
typicalBudget: { min: 25, max: 80 },
keywords: ['jõulud', 'jõulu', 'christmas', 'jõulukingitus']
},
{
name: 'Uusaasta', // New Year
date: '12-31',
typicalBudget: { min: 20, max: 60 },
keywords: ['uusaasta', 'uue aasta', 'new year', 'aastavahetuse']
}
];
Budget Inference Logic
Location: language.ts:300-328
static getBudgetHintByOccasion(occasion: string): BudgetRange | null {
if (!occasion) return null;
const normalizedOccasion = occasion.toLowerCase().trim();
// PRIORITY 1: Check Estonian cultural occasions first
for (const estonianOccasion of ESTONIAN_OCCASIONS) {
const matched = estonianOccasion.keywords.some(keyword =>
normalizedOccasion.includes(keyword)
);
if (matched) {
return {
min: estonianOccasion.typicalBudget.min,
max: estonianOccasion.typicalBudget.max,
hint: `${estonianOccasion.name} kingitus`
};
}
}
// PRIORITY 2: Check general occasion budgets
for (const [occasionKey, budget] of Object.entries(OCCASION_BUDGETS)) {
if (normalizedOccasion.includes(occasionKey)) {
return budget;
}
}
return null; // No budget hint available
}
General Occasion Budgets
Location: language.ts:138-150
const OCCASION_BUDGETS: Record<string, BudgetRange> = {
'sünnipäev': { min: 20, max: 50, hint: 'keskmine sünnipäevakingitus' },
'tänu': { min: 10, max: 30, hint: 'tähelepanuavaldus' },
'väike tähelepanu': { min: 5, max: 15, hint: 'väike kingitus' },
'sissekolimine': { min: 30, max: 80, hint: 'sissekolimiskingitus' },
'pulm': { min: 50, max: 150, hint: 'pulmakingitus' },
'pensionile minek': { min: 30, max: 80, hint: 'pensionile jäämise kingitus' },
'lapse sünd': { min: 30, max: 80, hint: 'beebi kingitus' },
'paranemine': { min: 10, max: 30, hint: 'tervenemissoovidega' },
'juubel': { min: 40, max: 100, hint: 'tähtpäevakingitus' },
'lõpetamine': { min: 25, max: 70, hint: 'lõpukingitus' },
'hobi': { min: 15, max: 50, hint: 'hobiga seotud' }
};
Cultural Significance
Why this matters:
- Estonian Father's Day is November 14 (not June like US)
- Estonian Mother's Day is May 9 (fixed date, not floating Sunday)
- Jaanipäev is a MAJOR holiday (equivalent to July 4th in US)
- Christmas Eve (Dec 24) is the main celebration (not Dec 25)
- Budget expectations differ by culture (e.g., wedding gifts: 50-150€ vs $100-300 in US)
Estonian Product Prioritization
Location: language.ts:420-481
How Estonian Products Get Boosted
When a user queries in Estonian, the system automatically prioritizes Estonian cultural products through score multipliers.
Example: Estonian novel by Jaan Kross published by Varrak:
- 1.15 (author) × 1.15 (category) × 1.1 (publisher) = 1.457× boost (+45.7%)
Boosting Strategy
Estonian products receive multiplicative score boosts when the user's language is Estonian:
static boostEstonianProducts(products: Product[], language: string): Product[] {
// Only boost for Estonian language users
if (language !== 'et') return products;
return products.map(product => {
let boost = 1.0;
// 1. Estonian Authors: +15% boost
const authors = product.authors?.toString().toLowerCase() || '';
if (authors.match(/\b(eesti|estonian)\b/i)) {
boost *= 1.15;
}
// 2. Estonian Category: +15% boost
const category = product.csv_category?.toString() || '';
if (category.includes('Eesti')) {
boost *= 1.15;
}
// 3. Estonian Publishers: +10% boost
if (this.isEstonianPublisher(product)) {
boost *= 1.1;
}
// Apply cumulative boost to score
if (boost > 1.0) {
const currentScore = product.score || 1.0;
return { ...product, score: currentScore * boost };
}
return product;
});
}
Case sensitivity: the category boost checks for the substring
"Eesti"with a capital E. Lowercaseeesticategories are not boosted in the current implementation.
Estonian Publisher Detection
Location: language.ts:462-481
private static isEstonianPublisher(product: Product): boolean {
const publisher = product.publisher?.toString().toLowerCase() || '';
const title = product.title?.toString().toLowerCase() || '';
const ESTONIAN_PUBLISHERS = [
'varrak', // Major Estonian publisher
'tänapäev', // Contemporary literature
'pegasus', // Children's books
'eesti raamat', // Estonian book
'kunst', // Art publisher
'avita', // Literary publisher
'postimees', // Newspaper/books
'kirjastus', // Generic "publisher" (Estonian word)
'eesti keele sihtasutus' // Estonian Language Foundation
];
// Check publisher field
for (const estonianPub of ESTONIAN_PUBLISHERS) {
if (publisher.includes(estonianPub)) {
return true;
}
}
// Check title for publisher mentions
for (const estonianPub of ESTONIAN_PUBLISHERS) {
if (title.includes(estonianPub)) {
return true;
}
}
return false;
}
Cumulative Boost Examples
| Product | Estonian Author | Estonian Category | Estonian Publisher | Total Boost | Final Multiplier |
|---|---|---|---|---|---|
| Estonian novel by Jaan Kross | Yes | Yes | Yes (Varrak) | 1.15 × 1.15 × 1.1 | 1.457× (+45.7%) |
| Estonian film (Eesti Film) | No | Yes | No | 1.0 × 1.15 × 1.0 | 1.15× (+15%) |
| Translation (foreign author) | No | No | Yes (Tänapäev) | 1.0 × 1.0 × 1.1 | 1.1× (+10%) |
| Foreign book (no local ties) | No | No | No | 1.0 × 1.0 × 1.0 | 1.0× (no boost) |
Why Multiplicative (not Additive)?
// BAD: Additive boosts
boost = 0.0;
if (estonianAuthor) boost += 0.15;
if (estonianCategory) boost += 0.15;
if (estonianPublisher) boost += 0.10;
finalScore = score * (1.0 + boost); // Max: 1.4× (linear)
// GOOD: Multiplicative boosts
boost = 1.0;
if (estonianAuthor) boost *= 1.15;
if (estonianCategory) boost *= 1.15;
if (estonianPublisher) boost *= 1.1;
finalScore = score * boost; // Max: 1.457× (compound)
Advantages:
- Products with multiple Estonian signals get disproportionately stronger boosts
- Reflects real-world preference: "Estonian author + Estonian publisher" is MORE valuable than sum of parts
- Prevents over-boosting products with single weak signal
Gender Appropriateness
Actual implementation (Phase 4.5, post-rerank):
- Gender is inferred via
detectGenderFromRecipientinapp/api/chat/services/gender-affinity.ts:255-320, invoked from the context orchestrator (app/api/chat/orchestrators/context-orchestrator/extract-context.ts:909-937). - Boosting is applied later in the search orchestrator (
app/api/chat/orchestrators/search-orchestrator.ts:474-528) using category affinity scores andcalculateGenderBoostMultiplier(0.5×–1.8×). This happens after Stage B and after rerank. - The helper
LanguageService.adjustScoreForGenderinlanguage.tsis currently unused in the pipeline; Stage B does not perform gender filtering or scoring.
Implications:
- Gender handling is a soft ranking boost, not a funnel/filter rule.
- Only products with categories present in
CATEGORY_GENDER_AFFINITY(gender-affinity.ts) receive adjustments; missing/neutral categories pass through unchanged. - If accurate gender-specific behavior is required earlier in the funnel, code changes would be needed; the document now reflects the current behavior.
Integration Points
1. Language Detection Layer
Location: language.ts:238-278
static detectLanguage(message: string): 'et' | 'en' | 'mixed' {
const estonianWords = /\b(soovita|soovitada|otsin|kingitus|raamat|...)\b/i;
const englishWords = /\b(recommend|search|want|gift|book|...)\b/i;
const hasEstonian = estonianWords.test(message);
const hasEnglish = englishWords.test(message);
if (hasEstonian && hasEnglish) return 'mixed';
if (hasEstonian) return 'et';
if (hasEnglish) return 'en';
return 'et'; // Default to Estonian for Estonian bookstore
}
Rules:
- Estonian query ('et') → Estonian prompt → Estonian response
- English query ('en') → English prompt → English response
- Mixed query ('mixed') → Estonian prompt → Estonian response (default to Estonian)
2. Stage B Funnel (Phase 2)
Location: app/api/chat/services/stage-b.ts
Cultural rules are retrieved and applied during funnel filtering:
function createStageBEnvironment(context: GiftContext, ...): StageBEnvironment {
return {
context,
debug,
excludeSet,
budgets: { ... },
product: { ... },
age: { ... },
allowAdultProducts,
isAdultRecipient,
culturalRules: LanguageService.getCulturalRules(), // ← Cultural rules injected here
negativeConstraintPatterns,
normalizedIntent
};
}
function passesStageBFilters(candidate, env, state): boolean {
if (!passesAgeBasedFilters(candidate, env, state)) return false; // ← Uses culturalRules
// ... other filters
}
3. Search Orchestrator (Phase 6)
Location: app/api/chat/orchestrators/search-orchestrator.ts:298-318
Estonian product boosting is applied after funnel and before reranking:
// Phase 6: Estonian Product Prioritization
if (SearchOrchestratorConfig.PHASE6_ENABLED && giftContext.language === 'et') {
const boostStart = Date.now();
searchResult.products = LanguageService.boostEstonianProducts(
searchResult.products as any[],
giftContext.language
) as any[];
if (debug) {
console.log('ESTONIAN BOOST APPLIED:', {
time: (Date.now() - boostStart) + 'ms',
products: searchResult.products.length,
language: giftContext.language
});
}
}
4. System Prompt Generation
Location: app/api/chat/system-prompt.ts:246-292
Language-aware system prompts:
export function generateDynamicSystemPrompt(
language?: 'et' | 'en' | 'mixed',
context?: { recipient?: string; occasion?: string; productType?: string }
): string {
// Rule: Estonian/mixed → Estonian prompt, English → English prompt
let basePrompt = (!language || language === 'mixed' || language === 'et')
? buildEstonianSystemPrompt()
: buildEnglishSystemPrompt();
// Add context reminders for multi-turn coherence
if (context) {
// Language-aware context reminders (Estonian vs English)
}
return basePrompt;
}
Pipeline Flow with Cultural Intelligence
Testing & Validation
Test Suite
Location: test-phase6-cultural-rules.sh
Dedicated test script for cultural rules and Estonian boost:
#!/bin/bash
# PHASE 6: Bilingual & Cultural Rules Test
# Tests: Estonian language support, cultural appropriateness, Estonian product prioritization
test_phase6() {
local test_name="$1"
local query="$2"
local expected_language="$3" # et, en, mixed
local expect_estonian_boost="$4" # true/false
local cultural_context="$5" # Description of cultural expectation
# Make API request with debug=true
response=$(curl -s -X POST "$API_URL" \
-H "Content-Type: application/json" \
-d "{
\"messages\": [{\"role\": \"user\", \"content\": \"$query\"}],
\"conversationId\": \"test-phase6-$(date +%s)\",
\"debug\": true
}")
# Extract language detection
detected_lang=$(echo "$response" | grep -o '"language":"[^"]*"' | cut -d'"' -f4)
# Check for Estonian boost
estonian_boost_applied=$(echo "$response" | grep -q "ESTONIAN BOOST APPLIED" && echo "true" || echo "false")
# Validate results
# ...
}
Test Scenarios
1. Language Detection Tests
test_phase6 "Estonian Query" \
"Soovita raamat lapsele" \
"et" \
"true" \
"Estonian language → Estonian boost"
test_phase6 "English Query" \
"Recommend a book for a child" \
"en" \
"false" \
"English language → No Estonian boost"
test_phase6 "Mixed Query" \
"Soovita book for lapsele" \
"mixed" \
"true" \
"Mixed language → Default to Estonian (boost applied)"
2. Age-Based Cultural Filtering Tests
test_phase6 "Child Recipient (No Alcohol)" \
"Kingitus 10-aastasele poisile" \
"et" \
"true" \
"Child → Alcohol filtered out"
test_phase6 "Elderly Recipient (No Toys)" \
"Kingitus 75-aastasele vanaemale" \
"et" \
"true" \
"Elderly → Gaming/toys filtered out"
test_phase6 "Teen Recipient (No Baby Products)" \
"Kingitus 15-aastasele tüdrukule" \
"et" \
"true" \
"Teen → Baby products filtered out"
3. Estonian Occasion Tests
test_phase6 "Jaanipäev (Estonian Holiday)" \
"Jaanipäeva kingitus kolleegile" \
"et" \
"true" \
"Jaanipäev → Budget 15-40€ inferred"
test_phase6 "Isadepäev (Estonian Father's Day)" \
"Isadepäeva kingitus" \
"et" \
"true" \
"Isadepäev → Budget 20-60€ inferred (November 14)"
4. Estonian Product Boost Tests
test_phase6 "Estonian Author Query" \
"Andrus Kivirähk raamat" \
"et" \
"true" \
"Estonian author → +15% boost"
test_phase6 "Estonian Film Query" \
"Eesti filmid" \
"et" \
"true" \
"Estonian category → +15% boost"
Manual Test Scenarios
Location: test-scenarios/comprehensive-test-scenarios.md
Documented test cases for cultural relevance:
### AGE-CULTURE-001: Elderly Recipient (No Gaming)
**Query:** "Kingitus 70-aastasele vanaisale"
**Expected:** No gaming, toys, or baby products
**Validation:** Check that elderlyForbidden categories are filtered
### AGE-CULTURE-002: Child Recipient (No Alcohol)
**Query:** "Kingitus 8-aastasele lapsele"
**Expected:** No alcohol products
**Validation:** Check that childForbidden categories are filtered
### OCCASION-CULTURE-001: Jaanipäev Budget
**Query:** "Jaanipäeva kingitus"
**Expected:** Budget inferred as 15-40€
**Validation:** Check context.budgetMin === 15, budgetMax === 40
### ESTONIAN-BOOST-001: Estonian Author
**Query:** "Soovita Jaan Kross raamat"
**Expected:** Estonian author products ranked higher
**Validation:** Check for +15% boost in logs
Test Coverage
| Feature | Test Type | Location | Coverage |
|---|---|---|---|
| Language Detection | Manual curl smoke (logs inspected) | test-phase6-cultural-rules.sh | Informational only |
| Age Filtering | Manual curl + scenario docs | test-phase6-cultural-rules.sh, test-scenarios/ | Informational only |
| Occasion Budget | Manual | test-scenarios/comprehensive-test-scenarios.md | Informational only |
| Estonian Boost | Manual curl (log check for boost) | test-phase6-cultural-rules.sh | Informational only |
| Gender Ranking Boost | Manual scenarios | test-scenarios/ | Partial; no automated checks |
Performance Metrics
No repository-backed production metrics or load-test artifacts are available for this system. All latency/uptime figures are TBD until instrumentation or benchmark logs are added.
Future Enhancements
Planned Improvements (Q1 2026)
1. Dynamic Cultural Rules
Current: Hardcoded in language.ts
Proposed: Database-backed with admin UI
// Future: Load cultural rules from database
const culturalRules = await db.query('culturalRules')
.filter(q => q.eq('market', 'estonia'))
.filter(q => q.eq('active', true))
.collect();
Benefits:
- Market-specific rules without code changes
- A/B testing different cultural thresholds
- Rapid response to cultural sensitivities
2. Cultural Fit Scoring
Current: Binary pass/fail filtering
Proposed: Graduated cultural fit scores (0-1)
interface CulturalFitScore {
ageAppropriateness: number; // 0.0 - 1.0
occasionRelevance: number; // 0.0 - 1.0
localCulturalValue: number; // 0.0 - 1.0
genderAppropriate: number; // 0.0 - 1.0
total: number; // weighted average
}
function calculateCulturalFit(product, context): CulturalFitScore {
// Graduated scoring instead of hard filters
}
Benefits:
- More nuanced rankings (not just "pass" or "fail")
- Better handling of edge cases
- Explainable cultural relevance
3. Regional Variations
Current: Estonia-wide rules
Proposed: Support for regional differences
const REGIONAL_OCCASIONS = {
'tartu': [
{ name: 'Tartu Linnapäev', date: '06-29', budget: { min: 15, max: 35 } }
],
'tallinn': [
{ name: 'Tallinna Linnapäev', date: '05-15', budget: { min: 20, max: 50 } }
],
// ... other cities
};
Benefits:
- Hyper-local relevance
- Support for city-specific holidays
- Regional gift budget differences
4. Cultural Explanation in Responses
Current: Cultural filtering happens silently
Proposed: Surface cultural reasoning to users
interface ProductRecommendation {
product: Product;
relevanceScore: number;
culturalFit: {
score: number;
explanation: string; // NEW
badges: string[]; // NEW: ['Estonian Author', 'Age-Appropriate', 'Local Publisher']
};
}
Example Response:
"This book by Andrus Kivirähk is from Estonian publisher Varrak
and perfect for Jaanipäev (budget: €25). The content is appropriate
for adults and aligns with Estonian literary traditions."
5. Multi-Market Support
Current: Estonian-only cultural intelligence
Proposed: Extensible to other markets
interface MarketCulturalRules {
market: 'estonia' | 'finland' | 'latvia' | 'lithuania';
language: string;
occasions: EstonianOccasion[];
ageRules: CulturalRules;
localPublishers: string[];
budgetCurrency: string;
}
const MARKET_RULES: Record<string, MarketCulturalRules> = {
estonia: { ... },
finland: { ... }, // Future expansion
latvia: { ... } // Future expansion
};
Research Directions
1. LLM-Based Cultural Filtering
Hypothesis: LLMs can handle nuanced cultural edge cases better than rule-based systems
Experiment:
// Current: Rule-based filtering (fast, deterministic)
if (culturalRules.elderlyForbidden.some(kw => title.includes(kw))) {
return false;
}
// Proposed: LLM-based filtering (slower, more nuanced)
const culturalFit = await llm.evaluate(`
Is "${product.title}" culturally appropriate for a 70-year-old Estonian man?
Consider: age appropriateness, Estonian cultural norms, social expectations.
Respond with: APPROPRIATE, QUESTIONABLE, or INAPPROPRIATE.
`);
Challenges:
- Latency (50-200ms per product)
- Cost ($0.01 per 1000 products at scale)
- Reliability (LLM hallucinations)
2. User Feedback Loop
Proposal: Learn from user behavior to refine cultural rules
// Track user interactions
interface UserFeedback {
userId: string;
productId: string;
query: string;
culturalFitScore: number;
userAction: 'clicked' | 'purchased' | 'skipped';
timestamp: Date;
}
// Analyze patterns
const insights = analyzeUserFeedback({
timeRange: 'last_30_days',
segment: 'elderly_recipients'
});
// Adjust cultural rules based on insights
if (insights.elderlyGamingAcceptance > 0.3) {
// Consider removing "gaming" from elderlyForbidden
}
3. Cross-Cultural Gift Intelligence
Scenario: Estonian user buying gift for someone in Finland
interface CrossCulturalContext {
giverMarket: 'estonia';
recipientMarket: 'finland';
occasion: 'birthday';
culturalTranslation: {
budgetAdjustment: 1.2, // Finnish gifts 20% more expensive
occasionMapping: { 'sünnipäev' → 'syntymäpäivä' },
culturalNotes: ['Finns prefer practical gifts', 'Avoid overly personal items']
};
}
Appendix
A. Estonian Morphology
Estonian is a highly inflected language with 14 grammatical cases. The Cultural Relevance System handles dative case (allatiiw) for gift queries:
// Dative case: "to someone" (kellele? / to whom?)
const ESTONIAN_DATIVE_MAP: Record<string, string> = {
'õpetaja': 'õpetajale', // teacher → to teacher
'sõber': 'sõbrale', // friend → to friend
'ema': 'emale', // mother → to mother
'isa': 'isale', // father → to father
'laps': 'lapsele', // child → to child
// ... 20+ mappings
};
// Usage: "kingitus õpetajale" (gift to teacher)
B. Synonym Normalization
English → Estonian canonical form mapping:
// Recipients
{ pattern: /\b(grandmother|granny)\b/i, canonical: 'vanaema' }
{ pattern: /\b(grandfather|granddad)\b/i, canonical: 'vanaisa' }
{ pattern: /\b(mother|mom)\b/i, canonical: 'ema' }
// Occasions
{ pattern: /\b(birthday|b-day)\b/i, canonical: 'sünnipäev' }
{ pattern: /\b(wedding|marriage)\b/i, canonical: 'pulm' }
C. Cultural Filtering Decision Tree
D. Related Documentation
- Main Documentation:
documentation/docs/quality/estonian-cultural.md - Challenges Overview:
architecture/estonian-language-llm-challenges.md - Best Practices:
documentation/docs/quality/estonian-best-practices.md - Test Scenarios:
test-scenarios/comprehensive-test-scenarios.md - Implementation:
app/api/chat/services/language.ts - Funnel Integration:
app/api/chat/services/stage-b.ts
E. Key Metrics (November 2025)
| Metric | Value | Notes |
|---|---|---|
| Languages Supported | 2 (Estonian, English) | + mixed detection |
| Estonian Occasions | 9 | Culturally-specific holidays |
| Cultural Rules | 3 categories | Elderly, Child, Teen |
| Forbidden Keywords | 25+ | Age-inappropriate terms |
| Estonian Publishers | 9 | Major Estonian publishers |
| Dative Case Mappings | 20+ | Estonian morphology |
| Test Coverage | Manual smoke only | No automated coverage metrics in repo |
| Production Uptime | N/A | Not instrumented in repo |
Conclusion
The Cultural Relevance System represents a sophisticated, multi-layered approach to cultural intelligence that goes far beyond simple translation or localization. By combining:
- Hard cultural filtering (safety & appropriateness)
- Estonian occasion awareness (local holidays & budgets)
- Local product prioritization (Estonian authors & publishers)
- Gender sensitivity (category-based ranking boost in Phase 4.5)
...the system delivers recommendations that feel native to Estonian culture rather than generic international suggestions.
Key Success Factors
Estonian-first design - Built for Estonian market (not adapted from Western defaults)
Early enforcement - Cultural rules applied in funnel stage (before expensive operations)
Transparent & debuggable - Logging for cultural decisions (when debug is on)
Testing status: Manual smoke checks only; automated coverage not yet reported
Performance status: Throughput/latency metrics not instrumented in repo
Document Version: 1.0
Last Reviewed: November 17, 2025
Next Review: March 1, 2026
Owner: Cultural Intelligence Team
Reviewers: Product, Engineering, Estonian Cultural Advisors