#!/usr/bin/env node
/**
* @module Intelligence Operations/Data Transformation Pipeline
* @category Intelligence Operations - Intelligence Data Transformation
*
* @description
* Core data transformation pipeline converting raw MCP server responses into
* structured intelligence article content. This module implements advanced semantic
* processing algorithms for legislative data, parliamentary event analysis, and
* multi-dimensional data mapping for automated journalism.
*
* The transformation pipeline provides four specialized processing stages:
*
* Stage 1 - Calendar Event Processing (transformCalendarToEventGrid):
* Transforms raw calendar data from riksdag-regering-mcp into structured event grid
* suitable for visual presentation. Handles multiple timestamp formats (MCP responses
* may use 'datum', 'from', 'start' fields), performs temporal normalization, and
* groups events by date for calendar visualization. Implements date comparison logic
* for marking "today" events with visual indicators.
*
* Stage 2 - Document Content Generation (generateArticleContent):
* Processes structured parliamentary documents into narrative article prose. Maps
* document types (propositions, motions, reports) to narrative structures, extracts
* semantic meaning from legislative language, and generates coherent paragraphs
* suitable for journalist review. Applies natural language processing techniques
* for readability optimization and audience targeting.
*
* Stage 3 - Intelligence Extraction (extractWatchPoints):
* Performs analytical extraction of critical intelligence points from parliamentary
* data. Identifies policy implications, fiscal impacts, timeline constraints, and
* political risk factors. Uses rule-based analysis for common legislative patterns
* (votes, committee decisions, government actions) and produces structured watch
* points for inclusion in article "watch sections".
*
* Stage 4 - Metadata Generation (generateMetadata, calculateReadTime, generateSources):
* Synthesizes article metadata including publication date, author attribution, reading
* time estimates, source citations, and SEO keywords. Generates machine-readable
* metadata for structured data (Schema.org JSON-LD) and social media sharing.
*
* Supported Data Types:
* - Calendar events (committee meetings, plenary sessions, parliamentary breaks)
* - Legislative documents (propositions, motions, parliamentary inquiries)
* - Voting records (roll-call votes with party/member positions)
* - Government announcements (press releases, policy documents, ministerial statements)
* - Committee reports (analysis, recommendations, decisions)
* - Debate transcripts (parliamentary speeches with speaker context)
*
* Multi-Language Processing:
* - Swedish source content transformation into 14 target languages
* - Terminology mapping for political/legal concepts
* - Date formatting and timezone adjustment per target language
* - Pluralization and grammatical agreement handling
* - RTL language support for Arabic and Hebrew output
*
* Data Validation & Quality Assurance:
* - Schema validation against CIA data model definitions
* - Null/undefined field handling with intelligent fallbacks
* - Temporal consistency checking (dates in correct order)
* - Cross-reference validation (referenced documents exist)
* - Semantic completeness assessment
*
* @intelligence
* Semantic Processing Methodology:
*
* Legislative Intent Analysis:
* Extracts implicit meaning from formal parliamentary language through:
* - Keyword detection for policy domains (fiscal, healthcare, defense, etc.)
* - Stakeholder identification (ministries, agencies, party groups)
* - Impact type classification (regulatory, fiscal, social)
* - Timeline extraction (implementation dates, decision deadlines)
* - Precedent linking (related historical legislative actions)
*
* Party Position Inference:
* Maps voting records and committee recommendations to political positions:
* - Consensus detection (all parties agree vs. split votes)
* - Coalition formation analysis (which parties vote together)
* - Opposition mapping (which parties consistently oppose)
* - Swing vote identification (MPs changing position across votes)
*
* Risk Indicator Extraction:
* Identifies critical intelligence points:
* - Fiscal implications and budget impacts
* - Timeline constraints and urgent decisions
* - Stakeholder conflicts and controversy indicators
* - Implementation risks and dependency chains
* - Political feasibility assessments
*
* Content Generation Patterns:
* - Inverted pyramid structure (most important facts first)
* - Narrative coherence preservation across transformations
* - Tone consistency (journalistic neutrality in automated output)
* - Rhetorical device detection and adaptation
*
* @osint
* Source Data Processing Strategies:
*
* MCP API Response Handling:
* - Graceful handling of incomplete or malformed MCP responses
* - Field mapping flexibility for varying API versions
* - Timestamp normalization across multiple formats
* - Array and object structure flattening for template consumption
*
* Data Quality Assessment:
* - Completeness scoring (what percentage of fields populated?)
* - Freshness validation (data collection timestamp vs. processing time)
* - Consistency checking (cross-field validation)
* - Accuracy verification (comparison against official sources where possible)
*
* Source Attribution:
* - MCP tool reference tracking (which API call produced this data?)
* - Data timestamp preservation (when was this data collected?)
* - Source URL generation for primary documents
* - Author/department attribution for government documents
*
* @risk
* Data Transformation Risks & Mitigations:
*
* Threat: Semantic Loss in Translation
* - Complex political concepts losing nuance in transformation
* - Mitigation: Preserve original language for key terms, human review process
*
* Threat: Data Hallucination
* - Algorithm generating plausible but incorrect inferences
* - Mitigation: Strict fact-based extraction, no speculative inference
*
* Threat: Timestamp Ambiguity
* - Multiple timestamp fields with different meanings causing confusion
* - Mitigation: Explicit field mapping, validation against known formats
*
* Threat: Array Data Loss
* - Simplified array flattening losing important structure
* - Mitigation: Preserve hierarchical structure in intermediate representations
*
* Threat: Language Pair Incompleteness
* - Missing translations causing incomplete or English article fallback
* - Mitigation: Fallback chain (target > Swedish > English), quality validation
*
* @gdpr
* Data Processing Compliance:
*
* - Personal Data Exclusion:
* * Exclude contact information, addresses, phone numbers
* * Exclude email addresses and social media handles
* * Process public officials in official capacity only
*
* - Data Minimization:
* * Extract only necessary fields for article generation
* * Remove internal government identifiers and batch IDs
* * Exclude audit logs and technical metadata
*
* - Purpose Limitation:
* * Generate public articles only
* * No profiling or behavioral analysis
* * No commercial use beyond journalism platform
*
* - Processing Transparency:
* * Document all transformation rules
* * Publish source attribution with articles
* * Maintain audit trail via Git
*
* @security
* Content Security Implementation:
*
* Input Validation:
* - Type checking for all input parameters
* - Array/object structure validation before processing
* - String content sanitization via escapeHtml()
* - Numeric field validation (dates, counts, percentages)
*
* Output Encoding:
* - HTML entity escaping for all narrative text
* - URL encoding for generated links
* - No code injection vectors in generated content
* - CSS selector sanitization for class/ID generation
*
* Dependency Security:
* - html-utils module provides sanitization
* - No eval() or Function() constructor usage
* - No dynamic require() or import() patterns
* - Direct module imports only
*
* @author Hack23 AB - Intelligence Operations Team
* @license Apache-2.0
* @version 2.0.0
*
* @see {@link ./mcp-client.js} MCP API client providing raw data
* @see {@link ./article-template.js} Template rendering consuming transformed data
* @see {@link ./generate-news-enhanced.js} Article generation orchestration
* @see {@link ./html-utils.js} HTML sanitization (escapeHtml)
* @see {@link docs/DATA_TRANSFORMATION_GUIDE.md} Detailed transformation algorithms
* @see {@link docs/MCP_DATA_SCHEMA.md} MCP response schema definitions
* @see {@link docs/INTELLIGENCE_EXTRACTION.md} Intelligence analysis methodology
*/
import { escapeHtml } from './html-utils.js';
/**
* Transform calendar events into event grid structure for template
*
* @param {Array} events - Calendar events from MCP server
* @param {string} lang - Language code (en, sv)
* @returns {Array} Event grid structure for article template
*/
export function transformCalendarToEventGrid(events, lang = 'en') {
if (!events || events.length === 0) return [];
// Group events by date
const eventsByDate = {};
events.forEach(event => {
// Extract date from various field formats (MCP responses use 'from', 'start', or 'datum')
let dateStr = event.datum || event.from || event.start;
if (dateStr) {
// Extract just the date part if it's an ISO timestamp
dateStr = dateStr.split('T')[0];
}
if (!dateStr) return;
if (!eventsByDate[dateStr]) {
eventsByDate[dateStr] = [];
}
eventsByDate[dateStr].push(event);
});
// Sort dates
const sortedDates = Object.keys(eventsByDate).sort();
// Convert to grid format
const eventGrid = sortedDates.map(date => {
const dateObj = new Date(date);
const isTodayFlag = isTodayDate(dateObj);
return {
date: date,
dayName: formatDayName(dateObj, lang),
dayNumber: dateObj.getDate().toString(),
dayLabel: formatDayLabel(dateObj, lang),
isToday: isTodayFlag,
items: eventsByDate[date].map(event => ({
time: event.tid || event.time || 'Expected',
title: event.rubrik || event.titel || event.title || 'Event'
}))
};
});
return eventGrid;
}
/**
* Check if date is today
*/
function isTodayDate(date) {
const today = new Date();
return date.getDate() === today.getDate() &&
date.getMonth() === today.getMonth() &&
date.getFullYear() === today.getFullYear();
}
/**
* Map of custom locale codes to Intl-compatible locale strings
*/
const LOCALE_MAP = {
en: 'en-GB', sv: 'sv-SE', da: 'da-DK', no: 'no-NO', fi: 'fi-FI',
de: 'de-DE', fr: 'fr-FR', es: 'es-ES', nl: 'nl-NL', ar: 'ar-SA',
he: 'he-IL', ja: 'ja-JP', ko: 'ko-KR', zh: 'zh-CN'
};
/**
* Format day name (Monday, Tuesday, etc.) using Intl for all 14 languages
*/
function formatDayName(date, lang = 'en') {
const locale = LOCALE_MAP[lang] || lang;
try {
return new Intl.DateTimeFormat(locale, { weekday: 'long' }).format(date);
} catch {
return new Intl.DateTimeFormat('en-GB', { weekday: 'long' }).format(date);
}
}
/**
* Format day label (e.g., "February 10 - Monday") using Intl for all 14 languages
*/
function formatDayLabel(date, lang = 'en') {
const locale = LOCALE_MAP[lang] || lang;
try {
const dayName = formatDayName(date, lang);
const monthDay = new Intl.DateTimeFormat(locale, { month: 'long', day: 'numeric' }).format(date);
return `${monthDay} - ${dayName}`;
} catch {
const dayName = formatDayName(date, 'en');
const monthDay = new Intl.DateTimeFormat('en-GB', { month: 'long', day: 'numeric' }).format(date);
return `${monthDay} - ${dayName}`;
}
}
/**
* Extract key topics from documents
*
* @param {Array} documents - Documents from MCP server
* @returns {Array} Topic tags
*/
export function extractTopics(documents) {
const topics = new Set();
documents.forEach(doc => {
// Extract from document type
if (doc.doktyp) {
switch (doc.doktyp) {
case 'mot': topics.add('motions'); break;
case 'prop': topics.add('propositions'); break;
case 'bet': topics.add('committee-reports'); break;
case 'skr': topics.add('government-communication'); break;
}
}
// Extract from organ/committee
if (doc.organ) {
topics.add(`${doc.organ.toLowerCase()}-committee`);
}
// Extract from title keywords
const title = (doc.titel || doc.rubrik || '').toLowerCase();
if (title.includes('eu')) topics.add('eu');
if (title.includes('försvar')) topics.add('defense');
if (title.includes('ekonomi')) topics.add('economy');
if (title.includes('miljö')) topics.add('environment');
if (title.includes('migration')) topics.add('migration');
if (title.includes('utbildning')) topics.add('education');
if (title.includes('vård')) topics.add('healthcare');
});
return Array.from(topics).slice(0, 10); // Max 10 topics
}
/**
* Generate article content from MCP data
*
* @param {Object} data - MCP data (events, documents, etc.)
* @param {string} type - Article type (week-ahead, committee-reports, etc.)
* @param {string} lang - Language code
* @returns {string} Article HTML content
*/
export function generateArticleContent(data, type, lang = 'en') {
switch (type) {
case 'week-ahead':
return generateWeekAheadContent(data, lang);
case 'committee-reports':
return generateCommitteeContent(data, lang);
case 'propositions':
return generatePropositionsContent(data, lang);
case 'motions':
return generateMotionsContent(data, lang);
default:
return generateGenericContent(data, lang);
}
}
/**
* Generate Week Ahead article content
*/
// Multi-language labels for content generation
export const CONTENT_LABELS = {
en: {
whyMatters: 'Why This Week Matters',
whyMattersDefault: 'This week features significant parliamentary activity with key debates, committee meetings, and government consultations that will shape Sweden\'s political landscape.',
keyEvents: 'Key Events This Week',
whatToWatch: 'What to Watch',
latestReports: 'Latest Committee Reports',
noReports: 'No committee reports available at this time.',
committee: 'Committee', document: 'Document',
reportDefault: 'Committee report on parliamentary matter.',
govProps: 'Government Propositions',
noProps: 'No government propositions available at this time.',
propDefault: 'Government proposal to Parliament.',
oppMotions: 'Opposition Motions',
noMotions: 'No opposition motions available at this time.',
author: 'Author', party: 'Party',
motionDefault: 'Parliamentary motion by opposition member.',
genericContent: 'Content generation in progress.',
monitorDev: 'Monitor developments and outcomes',
committeeDebates: 'Committee Debates',
committeeDebatesDesc: (n) => `${n} committee reports scheduled for chamber debate`,
govProposals: 'Government Proposals',
govProposalsDesc: (n) => `${n} new government propositions under review`,
weekAhead: 'Week Ahead', committeeReportsTag: 'Committee Reports',
govPropsTag: 'Government Propositions', oppMotionsTag: 'Opposition Motions',
// Enhanced summary labels
committeeReport: 'committee report',
on: 'on',
governmentProposition: 'Government proposition',
regarding: 'regarding',
referredTo: 'referred to',
motionBy: 'Motion by',
parliamentaryMotion: 'Parliamentary motion',
unknown: 'Unknown',
// Analytical narrative labels
reportsOverview: (n) => `The Swedish Parliament's committees have published ${n} new reports, reflecting ongoing legislative work across multiple policy areas.`,
reportSignificance: 'This report addresses',
readFullReport: 'Read the full report',
propsOverview: (n) => `The government has submitted ${n} new propositions to Parliament, each requiring committee review and chamber debate before potential adoption.`,
propSignificance: 'This proposition concerns',
readFullProp: 'Read the full proposition',
motionsOverview: (n) => `Opposition MPs have filed ${n} new motions, pressing the government on issues ranging across policy domains.`,
motionSignificance: 'This motion addresses',
readFullMotion: 'Read the full motion',
policyContext: 'Policy context',
filedBy: 'Filed by'
},
sv: {
whyMatters: 'Varför denna vecka är viktig',
whyMattersDefault: 'Denna vecka innehåller betydande parlamentarisk aktivitet med viktiga debatter, kommittémöten och regeringskonsultationer som kommer att forma Sveriges politiska landskap.',
keyEvents: 'Nyckelhändelser denna vecka',
whatToWatch: 'Vad man ska följa',
latestReports: 'Senaste kommittérapporter',
noReports: 'Inga kommittérapporter tillgängliga för tillfället.',
committee: 'Kommitté', document: 'Dokument',
reportDefault: 'Kommittérapport om riksdagsärende.',
govProps: 'Regeringens propositioner',
noProps: 'Inga regeringspropositioner tillgängliga för tillfället.',
propDefault: 'Regeringens förslag till riksdagen.',
oppMotions: 'Oppositionens motioner',
noMotions: 'Inga oppositionsmotioner tillgängliga för tillfället.',
author: 'Författare', party: 'Parti',
motionDefault: 'Riksdagsmotion av oppositionsmedlem.',
genericContent: 'Innehållsgenerering pågår.',
monitorDev: 'Övervaka utveckling och resultat',
committeeDebates: 'Kommittédebatter',
committeeDebatesDesc: (n) => `${n} kommittérapporter planerade för kammarens debatt`,
govProposals: 'Regeringsförslag',
govProposalsDesc: (n) => `${n} nya regeringspropositioner under granskning`,
weekAhead: 'Veckan som kommer', committeeReportsTag: 'Kommittérapporter',
govPropsTag: 'Regeringens propositioner', oppMotionsTag: 'Oppositionens motioner',
// Enhanced summary labels
committeeReport: 'kommittérapport',
on: 'om',
governmentProposition: 'Regeringens proposition',
regarding: 'angående',
referredTo: 'hänvisad till',
motionBy: 'Motion av',
parliamentaryMotion: 'Riksdagsmotion',
unknown: 'Okänd',
// Analytical narrative labels
reportsOverview: (n) => `Riksdagens utskott har publicerat ${n} nya betänkanden som speglar det pågående lagstiftningsarbetet inom flera politikområden.`,
reportSignificance: 'Detta betänkande behandlar',
readFullReport: 'Läs hela betänkandet',
propsOverview: (n) => `Regeringen har överlämnat ${n} nya propositioner till riksdagen, var och en kräver utskottsbehandling och kammardebatt.`,
propSignificance: 'Denna proposition avser',
readFullProp: 'Läs hela propositionen',
motionsOverview: (n) => `Oppositionsriksdagsledamöter har lämnat in ${n} nya motioner som pressar regeringen inom flera politikområden.`,
motionSignificance: 'Denna motion behandlar',
readFullMotion: 'Läs hela motionen',
policyContext: 'Politisk kontext',
filedBy: 'Inlämnad av'
},
da: {
whyMatters: 'Hvorfor denne uge er vigtig',
whyMattersDefault: 'Denne uge byder på vigtig parlamentarisk aktivitet med centrale debatter, udvalgsmøder og regeringskonsultationer.',
keyEvents: 'Vigtige begivenheder denne uge',
whatToWatch: 'Hvad man skal følge',
latestReports: 'Seneste udvalgsbetænkninger',
noReports: 'Ingen udvalgsbetænkninger tilgængelige på nuværende tidspunkt.',
committee: 'Udvalg', document: 'Dokument',
reportDefault: 'Udvalgsbetænkning om parlamentarisk sag.',
govProps: 'Regeringsforslag',
noProps: 'Ingen regeringsforslag tilgængelige på nuværende tidspunkt.',
propDefault: 'Regeringsforslag til parlamentet.',
oppMotions: 'Oppositionsforslag',
noMotions: 'Ingen oppositionsforslag tilgængelige på nuværende tidspunkt.',
author: 'Forfatter', party: 'Parti',
motionDefault: 'Parlamentarisk forslag fra oppositionsmedlem.',
genericContent: 'Indhold genereres.',
monitorDev: 'Overvåg udviklingen og resultaterne',
committeeDebates: 'Udvalgsedebatter',
committeeDebatesDesc: (n) => `${n} udvalgsbetænkninger planlagt til kammerdebat`,
govProposals: 'Regeringsforslag',
govProposalsDesc: (n) => `${n} nye regeringsforslag under behandling`,
weekAhead: 'Ugen fremover', committeeReportsTag: 'Udvalgsbetænkninger',
govPropsTag: 'Regeringsforslag', oppMotionsTag: 'Oppositionsforslag',
// Enhanced summary labels
committeeReport: 'udvalgsbetænkning',
on: 'om',
governmentProposition: 'Regeringsforslag',
regarding: 'vedrørende',
referredTo: 'henvist til',
motionBy: 'Forslag fra',
parliamentaryMotion: 'Parlamentarisk forslag',
unknown: 'Ukendt',
// Analytical narrative labels
reportsOverview: (n) => `Sverigesrigsdagens udvalg har offentliggjort ${n} nye betænkninger, der afspejler igangværende lovgivningsarbejde.`,
reportSignificance: 'Denne betænkning omhandler',
readFullReport: 'Læs hele betænkningen',
propsOverview: (n) => `Regeringen har fremsat ${n} nye lovforslag til parlamentet.`,
propSignificance: 'Dette forslag vedrører',
readFullProp: 'Læs hele forslaget',
motionsOverview: (n) => `Oppositionsmedlemmer har indgivet ${n} nye forslag.`,
motionSignificance: 'Dette forslag omhandler',
readFullMotion: 'Læs hele forslaget',
policyContext: 'Politisk kontekst',
filedBy: 'Indgivet af'
},
no: {
whyMatters: 'Hvorfor denne uken er viktig',
whyMattersDefault: 'Denne uken byr på viktig parlamentarisk aktivitet med sentrale debatter, komitémøter og regjeringskonsultasjoner.',
keyEvents: 'Viktige hendelser denne uken',
whatToWatch: 'Hva man bør følge med på',
latestReports: 'Siste komitéinnstillinger',
noReports: 'Ingen komitéinnstillinger tilgjengelige for øyeblikket.',
committee: 'Komité', document: 'Dokument',
reportDefault: 'Komitéinnstilling om parlamentarisk sak.',
govProps: 'Regjeringens proposisjoner',
noProps: 'Ingen regjeringsproposisjoner tilgjengelige for øyeblikket.',
propDefault: 'Regjeringens forslag til parlamentet.',
oppMotions: 'Opposisjonsforslag',
noMotions: 'Ingen opposisjonsforslag tilgjengelige for øyeblikket.',
author: 'Forfatter', party: 'Parti',
motionDefault: 'Parlamentarisk forslag fra opposisjonsmedlem.',
genericContent: 'Innholdsgenerering pågår.',
monitorDev: 'Overvåk utviklingen og resultatene',
committeeDebates: 'Komitédebatter',
committeeDebatesDesc: (n) => `${n} komitéinnstillinger planlagt for kammerdebatt`,
govProposals: 'Regjeringsforslag',
govProposalsDesc: (n) => `${n} nye regjeringsproposisjoner under vurdering`,
weekAhead: 'Uke fremover', committeeReportsTag: 'Komitéinnstillinger',
govPropsTag: 'Regjeringens proposisjoner', oppMotionsTag: 'Opposisjonsforslag',
// Enhanced summary labels
committeeReport: 'komitéinnstilling',
on: 'om',
governmentProposition: 'Regjeringens proposisjon',
regarding: 'vedrørende',
referredTo: 'henvist til',
motionBy: 'Forslag fra',
parliamentaryMotion: 'Parlamentarisk forslag',
unknown: 'Ukjent',
// Analytical narrative labels
reportsOverview: (n) => `Den svenske riksdagens komiteer har publisert ${n} nye innstillinger som gjenspeiler pågående lovgivningsarbeid.`,
reportSignificance: 'Denne innstillingen omhandler',
readFullReport: 'Les hele innstillingen',
propsOverview: (n) => `Regjeringen har fremmet ${n} nye proposisjoner til Stortinget.`,
propSignificance: 'Denne proposisjonen gjelder',
readFullProp: 'Les hele proposisjonen',
motionsOverview: (n) => `Opposisjonsmedlemmer har fremmet ${n} nye forslag.`,
motionSignificance: 'Dette forslaget omhandler',
readFullMotion: 'Les hele forslaget',
policyContext: 'Politisk kontekst',
filedBy: 'Innsendt av'
},
fi: {
whyMatters: 'Miksi tämä viikko on tärkeä',
whyMattersDefault: 'Tällä viikolla on merkittävää parlamentaarista toimintaa, johon kuuluu tärkeitä keskusteluja, valiokuntakokouksia ja hallituksen kuulemisia.',
keyEvents: 'Viikon tärkeimmät tapahtumat',
whatToWatch: 'Mitä seurata',
latestReports: 'Uusimmat valiokuntamietinnöt',
noReports: 'Ei valiokuntamietintöjä saatavilla tällä hetkellä.',
committee: 'Valiokunta', document: 'Asiakirja',
reportDefault: 'Valiokuntamietintö parlamentaarisesta asiasta.',
govProps: 'Hallituksen esitykset',
noProps: 'Ei hallituksen esityksiä saatavilla tällä hetkellä.',
propDefault: 'Hallituksen esitys eduskunnalle.',
oppMotions: 'Opposition aloitteet',
noMotions: 'Ei opposition aloitteita saatavilla tällä hetkellä.',
author: 'Tekijä', party: 'Puolue',
motionDefault: 'Opposition jäsenen eduskunta-aloite.',
genericContent: 'Sisältöä luodaan.',
monitorDev: 'Seuraa kehitystä ja tuloksia',
committeeDebates: 'Valiokuntakeskustelut',
committeeDebatesDesc: (n) => `${n} valiokuntamietintöä aikataulutettu täysistuntokeskusteluun`,
govProposals: 'Hallituksen esitykset',
govProposalsDesc: (n) => `${n} uutta hallituksen esitystä käsittelyssä`,
weekAhead: 'Tuleva viikko', committeeReportsTag: 'Valiokuntamietinnöt',
govPropsTag: 'Hallituksen esitykset', oppMotionsTag: 'Opposition aloitteet',
// Enhanced summary labels
committeeReport: 'valiokunnan mietintö',
on: 'aiheesta',
governmentProposition: 'Hallituksen esitys',
regarding: 'koskien',
referredTo: 'lähetetty valiokuntaan',
motionBy: 'Aloite',
parliamentaryMotion: 'Eduskunnan aloite',
unknown: 'Tuntematon',
// Analytical narrative labels
reportsOverview: (n) => `Ruotsin valtiopäivien valiokunnat ovat julkaisseet ${n} uutta mietintöä, jotka heijastavat meneillään olevaa lainsäädäntötyötä.`,
reportSignificance: 'Tämä mietintö käsittelee',
readFullReport: 'Lue koko mietintö',
propsOverview: (n) => `Hallitus on jättänyt ${n} uutta esitystä eduskunnalle.`,
propSignificance: 'Tämä esitys koskee',
readFullProp: 'Lue koko esitys',
motionsOverview: (n) => `Opposition kansanedustajat ovat jättäneet ${n} uutta aloitetta.`,
motionSignificance: 'Tämä aloite käsittelee',
readFullMotion: 'Lue koko aloite',
policyContext: 'Poliittinen konteksti',
filedBy: 'Jättänyt'
},
de: {
whyMatters: 'Warum diese Woche wichtig ist',
whyMattersDefault: 'Diese Woche bietet bedeutende parlamentarische Aktivitäten mit wichtigen Debatten, Ausschusssitzungen und Regierungskonsultationen.',
keyEvents: 'Wichtige Ereignisse diese Woche',
whatToWatch: 'Was zu beobachten ist',
latestReports: 'Neueste Ausschussberichte',
noReports: 'Derzeit keine Ausschussberichte verfügbar.',
committee: 'Ausschuss', document: 'Dokument',
reportDefault: 'Ausschussbericht über parlamentarische Angelegenheit.',
govProps: 'Regierungsvorlagen',
noProps: 'Derzeit keine Regierungsvorlagen verfügbar.',
propDefault: 'Regierungsvorlage an das Parlament.',
oppMotions: 'Oppositionsanträge',
noMotions: 'Derzeit keine Oppositionsanträge verfügbar.',
author: 'Autor', party: 'Partei',
motionDefault: 'Parlamentarischer Antrag eines Oppositionsmitglieds.',
genericContent: 'Inhaltserstellung läuft.',
monitorDev: 'Entwicklungen und Ergebnisse überwachen',
committeeDebates: 'Ausschussdebatten',
committeeDebatesDesc: (n) => `${n} Ausschussberichte für Plenardebatte geplant`,
govProposals: 'Regierungsvorlagen',
govProposalsDesc: (n) => `${n} neue Regierungsvorlagen in Prüfung`,
weekAhead: 'Woche Voraus', committeeReportsTag: 'Ausschussberichte',
govPropsTag: 'Regierungsvorlagen', oppMotionsTag: 'Oppositionsanträge',
// Enhanced summary labels
committeeReport: 'Ausschussbericht',
on: 'über',
governmentProposition: 'Regierungsvorlage',
regarding: 'bezüglich',
referredTo: 'verwiesen an',
motionBy: 'Antrag von',
parliamentaryMotion: 'Parlamentarischer Antrag',
unknown: 'Unbekannt',
// Analytical narrative labels
reportsOverview: (n) => `Die Ausschüsse des schwedischen Reichstags haben ${n} neue Berichte veröffentlicht, die laufende Gesetzgebungsarbeit widerspiegeln.`,
reportSignificance: 'Dieser Bericht befasst sich mit',
readFullReport: 'Den vollständigen Bericht lesen',
propsOverview: (n) => `Die Regierung hat ${n} neue Vorlagen an das Parlament übermittelt.`,
propSignificance: 'Diese Vorlage betrifft',
readFullProp: 'Die vollständige Vorlage lesen',
motionsOverview: (n) => `Oppositionsabgeordnete haben ${n} neue Anträge eingereicht.`,
motionSignificance: 'Dieser Antrag befasst sich mit',
readFullMotion: 'Den vollständigen Antrag lesen',
policyContext: 'Politischer Kontext',
filedBy: 'Eingereicht von'
},
fr: {
whyMatters: 'Pourquoi cette semaine est importante',
whyMattersDefault: 'Cette semaine est marquée par une activité parlementaire significative avec des débats clés, des réunions de commission et des consultations gouvernementales.',
keyEvents: 'Événements clés cette semaine',
whatToWatch: 'À suivre',
latestReports: 'Derniers rapports de commission',
noReports: 'Aucun rapport de commission disponible pour le moment.',
committee: 'Commission', document: 'Document',
reportDefault: 'Rapport de commission sur une affaire parlementaire.',
govProps: 'Propositions gouvernementales',
noProps: 'Aucune proposition gouvernementale disponible pour le moment.',
propDefault: 'Proposition du gouvernement au Parlement.',
oppMotions: 'Motions d\'opposition',
noMotions: 'Aucune motion d\'opposition disponible pour le moment.',
author: 'Auteur', party: 'Parti',
motionDefault: 'Motion parlementaire d\'un membre de l\'opposition.',
genericContent: 'Génération de contenu en cours.',
monitorDev: 'Suivre les développements et les résultats',
committeeDebates: 'Débats en commission',
committeeDebatesDesc: (n) => `${n} rapports de commission prévus pour débat en séance`,
govProposals: 'Propositions gouvernementales',
govProposalsDesc: (n) => `${n} nouvelles propositions gouvernementales à l'examen`,
weekAhead: 'Semaine à venir', committeeReportsTag: 'Rapports de commission',
govPropsTag: 'Propositions gouvernementales', oppMotionsTag: 'Motions d\'opposition',
// Enhanced summary labels
committeeReport: 'rapport de commission',
on: 'sur',
governmentProposition: 'Proposition gouvernementale',
regarding: 'concernant',
referredTo: 'renvoyée à',
motionBy: 'Motion de',
parliamentaryMotion: 'Motion parlementaire',
unknown: 'Inconnu',
// Analytical narrative labels
reportsOverview: (n) => `Les commissions du Riksdag suédois ont publié ${n} nouveaux rapports reflétant le travail législatif en cours.`,
reportSignificance: 'Ce rapport traite de',
readFullReport: 'Lire le rapport complet',
propsOverview: (n) => `Le gouvernement a soumis ${n} nouvelles propositions au Parlement.`,
propSignificance: 'Cette proposition concerne',
readFullProp: 'Lire la proposition complète',
motionsOverview: (n) => `Des députés de l\'opposition ont déposé ${n} nouvelles motions.`,
motionSignificance: 'Cette motion traite de',
readFullMotion: 'Lire la motion complète',
policyContext: 'Contexte politique',
filedBy: 'Déposé par'
},
es: {
whyMatters: 'Por qué esta semana es importante',
whyMattersDefault: 'Esta semana presenta actividad parlamentaria significativa con debates clave, reuniones de comisión y consultas gubernamentales.',
keyEvents: 'Eventos clave esta semana',
whatToWatch: 'Qué observar',
latestReports: 'Últimos informes de comisión',
noReports: 'No hay informes de comisión disponibles en este momento.',
committee: 'Comisión', document: 'Documento',
reportDefault: 'Informe de comisión sobre asunto parlamentario.',
govProps: 'Proposiciones gubernamentales',
noProps: 'No hay proposiciones gubernamentales disponibles en este momento.',
propDefault: 'Propuesta del gobierno al Parlamento.',
oppMotions: 'Mociones de oposición',
noMotions: 'No hay mociones de oposición disponibles en este momento.',
author: 'Autor', party: 'Partido',
motionDefault: 'Moción parlamentaria de un miembro de la oposición.',
genericContent: 'Generación de contenido en curso.',
monitorDev: 'Monitorear desarrollos y resultados',
committeeDebates: 'Debates en comisión',
committeeDebatesDesc: (n) => `${n} informes de comisión programados para debate en pleno`,
govProposals: 'Propuestas gubernamentales',
govProposalsDesc: (n) => `${n} nuevas proposiciones gubernamentales en revisión`,
weekAhead: 'Semana próxima', committeeReportsTag: 'Informes de comisión',
govPropsTag: 'Proposiciones gubernamentales', oppMotionsTag: 'Mociones de oposición',
// Enhanced summary labels
committeeReport: 'informe de comisión',
on: 'sobre',
governmentProposition: 'Proposición gubernamental',
regarding: 'referente a',
referredTo: 'remitida a',
motionBy: 'Moción de',
parliamentaryMotion: 'Moción parlamentaria',
unknown: 'Desconocido',
// Analytical narrative labels
reportsOverview: (n) => `Las comisiones del Riksdag sueco han publicado ${n} nuevos informes que reflejan el trabajo legislativo en curso.`,
reportSignificance: 'Este informe aborda',
readFullReport: 'Leer el informe completo',
propsOverview: (n) => `El gobierno ha presentado ${n} nuevas proposiciones al Parlamento.`,
propSignificance: 'Esta proposición se refiere a',
readFullProp: 'Leer la proposición completa',
motionsOverview: (n) => `Diputados de la oposición han presentado ${n} nuevas mociones.`,
motionSignificance: 'Esta moción aborda',
readFullMotion: 'Leer la moción completa',
policyContext: 'Contexto político',
filedBy: 'Presentada por'
},
nl: {
whyMatters: 'Waarom deze week belangrijk is',
whyMattersDefault: 'Deze week biedt belangrijke parlementaire activiteit met cruciale debatten, commissievergaderingen en regeringsconsultaties.',
keyEvents: 'Belangrijke gebeurtenissen deze week',
whatToWatch: 'Wat te volgen',
latestReports: 'Nieuwste commissierapporten',
noReports: 'Geen commissierapporten beschikbaar op dit moment.',
committee: 'Commissie', document: 'Document',
reportDefault: 'Commissierapport over parlementaire zaak.',
govProps: 'Regeringsvoorstellen',
noProps: 'Geen regeringsvoorstellen beschikbaar op dit moment.',
propDefault: 'Regeringsvoorstel aan het parlement.',
oppMotions: 'Oppositiemoties',
noMotions: 'Geen oppositiemoties beschikbaar op dit moment.',
author: 'Auteur', party: 'Partij',
motionDefault: 'Parlementaire motie van een oppositielid.',
genericContent: 'Inhoud wordt gegenereerd.',
monitorDev: 'Ontwikkelingen en resultaten volgen',
committeeDebates: 'Commissiedebatten',
committeeDebatesDesc: (n) => `${n} commissierapporten gepland voor plenair debat`,
govProposals: 'Regeringsvoorstellen',
govProposalsDesc: (n) => `${n} nieuwe regeringsvoorstellen in behandeling`,
weekAhead: 'Week vooruit', committeeReportsTag: 'Commissierapporten',
govPropsTag: 'Regeringsvoorstellen', oppMotionsTag: 'Oppositiemoties',
// Enhanced summary labels
committeeReport: 'commissierapport',
on: 'over',
governmentProposition: 'Regeringsvoorstel',
regarding: 'betreffende',
referredTo: 'doorverwezen naar',
motionBy: 'Motie van',
parliamentaryMotion: 'Parlementaire motie',
unknown: 'Onbekend',
// Analytical narrative labels
reportsOverview: (n) => `De commissies van de Zweedse Riksdag hebben ${n} nieuwe rapporten gepubliceerd die het lopende wetgevingswerk weerspiegelen.`,
reportSignificance: 'Dit rapport behandelt',
readFullReport: 'Lees het volledige rapport',
propsOverview: (n) => `De regering heeft ${n} nieuwe voorstellen bij het parlement ingediend.`,
propSignificance: 'Dit voorstel betreft',
readFullProp: 'Lees het volledige voorstel',
motionsOverview: (n) => `Oppositieleden hebben ${n} nieuwe moties ingediend.`,
motionSignificance: 'Deze motie behandelt',
readFullMotion: 'Lees de volledige motie',
policyContext: 'Politieke context',
filedBy: 'Ingediend door'
},
ar: {
whyMatters: 'لماذا هذا الأسبوع مهم',
whyMattersDefault: 'يتميز هذا الأسبوع بنشاط برلماني كبير يشمل مناقشات رئيسية واجتماعات لجان ومشاورات حكومية.',
keyEvents: 'الأحداث الرئيسية هذا الأسبوع',
whatToWatch: 'ما يجب متابعته',
latestReports: 'أحدث تقارير اللجان',
noReports: 'لا توجد تقارير لجان متاحة حالياً.',
committee: 'اللجنة', document: 'الوثيقة',
reportDefault: 'تقرير لجنة عن مسألة برلمانية.',
govProps: 'مقترحات الحكومة',
noProps: 'لا توجد مقترحات حكومية متاحة حالياً.',
propDefault: 'مقترح حكومي للبرلمان.',
oppMotions: 'اقتراحات المعارضة',
noMotions: 'لا توجد اقتراحات معارضة متاحة حالياً.',
author: 'المؤلف', party: 'الحزب',
motionDefault: 'اقتراح برلماني من عضو في المعارضة.',
genericContent: 'جارٍ إنشاء المحتوى.',
monitorDev: 'متابعة التطورات والنتائج',
committeeDebates: 'مناقشات اللجان',
committeeDebatesDesc: (n) => `${n} تقارير لجان مجدولة للمناقشة في الجلسة العامة`,
govProposals: 'مقترحات حكومية',
govProposalsDesc: (n) => `${n} مقترحات حكومية جديدة قيد المراجعة`,
weekAhead: 'الأسبوع القادم', committeeReportsTag: 'تقارير اللجان',
govPropsTag: 'مقترحات الحكومة', oppMotionsTag: 'اقتراحات المعارضة',
// Enhanced summary labels
committeeReport: 'تقرير لجنة',
on: 'بشأن',
governmentProposition: 'مقترح حكومي',
regarding: 'فيما يتعلق بـ',
referredTo: 'محال إلى',
motionBy: 'اقتراح من',
parliamentaryMotion: 'اقتراح برلماني',
unknown: 'غير معروف',
// Analytical narrative labels
reportsOverview: (n) => `نشرت لجان البرلمان السويدي ${n} تقارير جديدة تعكس العمل التشريعي الجاري.`,
reportSignificance: 'يتناول هذا التقرير',
readFullReport: 'قراءة التقرير الكامل',
propsOverview: (n) => `قدمت الحكومة ${n} مقترحات جديدة إلى البرلمان.`,
propSignificance: 'يتعلق هذا المقترح بـ',
readFullProp: 'قراءة المقترح الكامل',
motionsOverview: (n) => `قدم أعضاء المعارضة ${n} اقتراحات جديدة.`,
motionSignificance: 'يتناول هذا الاقتراح',
readFullMotion: 'قراءة الاقتراح الكامل',
policyContext: 'السياق السياسي',
filedBy: 'مقدم من'
},
he: {
whyMatters: 'למה השבוע הזה חשוב',
whyMattersDefault: 'השבוע כולל פעילות פרלמנטרית משמעותית עם דיונים מרכזיים, ישיבות ועדה והתייעצויות ממשלתיות.',
keyEvents: 'אירועים מרכזיים השבוע',
whatToWatch: 'מה לעקוב אחריו',
latestReports: 'דוחות ועדה אחרונים',
noReports: 'אין דוחות ועדה זמינים כרגע.',
committee: 'ועדה', document: 'מסמך',
reportDefault: 'דוח ועדה בנושא פרלמנטרי.',
govProps: 'הצעות ממשלה',
noProps: 'אין הצעות ממשלה זמינות כרגע.',
propDefault: 'הצעת ממשלה לפרלמנט.',
oppMotions: 'הצעות אופוזיציה',
noMotions: 'אין הצעות אופוזיציה זמינות כרגע.',
author: 'מחבר', party: 'מפלגה',
motionDefault: 'הצעה פרלמנטרית של חבר אופוזיציה.',
genericContent: 'יצירת תוכן בתהליך.',
monitorDev: 'לעקוב אחר התפתחויות ותוצאות',
committeeDebates: 'דיוני ועדות',
committeeDebatesDesc: (n) => `${n} דוחות ועדה מתוכננים לדיון במליאה`,
govProposals: 'הצעות ממשלה',
govProposalsDesc: (n) => `${n} הצעות ממשלה חדשות בבחינה`,
weekAhead: 'השבוע הקרוב', committeeReportsTag: 'דוחות ועדה',
govPropsTag: 'הצעות ממשלה', oppMotionsTag: 'הצעות אופוזיציה',
// Enhanced summary labels
committeeReport: 'דוח ועדה',
on: 'על',
governmentProposition: 'הצעת ממשלה',
regarding: 'בנוגע ל',
referredTo: 'הועבר ל',
motionBy: 'הצעה של',
parliamentaryMotion: 'הצעה פרלמנטרית',
unknown: 'לא ידוע',
// Analytical narrative labels
reportsOverview: (n) => `ועדות הריקסדאג השוודי פרסמו ${n} דוחות חדשים המשקפים עבודת חקיקה שוטפת.`,
reportSignificance: 'דוח זה עוסק ב',
readFullReport: 'קראו את הדוח המלא',
propsOverview: (n) => `הממשלה הגישה ${n} הצעות חדשות לפרלמנט.`,
propSignificance: 'הצעה זו נוגעת ל',
readFullProp: 'קראו את ההצעה המלאה',
motionsOverview: (n) => `חברי אופוזיציה הגישו ${n} הצעות חדשות.`,
motionSignificance: 'הצעה זו עוסקת ב',
readFullMotion: 'קראו את ההצעה המלאה',
policyContext: 'הקשר מדיני',
filedBy: 'הוגשה על ידי'
},
ja: {
whyMatters: 'なぜ今週が重要か',
whyMattersDefault: '今週は重要な議会活動があり、主要な討論、委員会会議、政府協議が予定されています。',
keyEvents: '今週の主要イベント',
whatToWatch: '注目すべきポイント',
latestReports: '最新の委員会報告',
noReports: '現在、委員会報告はありません。',
committee: '委員会', document: '文書',
reportDefault: '議会事案に関する委員会報告。',
govProps: '政府提案',
noProps: '現在、政府提案はありません。',
propDefault: '政府から議会への提案。',
oppMotions: '野党動議',
noMotions: '現在、野党動議はありません。',
author: '著者', party: '政党',
motionDefault: '野党議員による議会動議。',
genericContent: 'コンテンツ生成中。',
monitorDev: '動向と結果を監視',
committeeDebates: '委員会討論',
committeeDebatesDesc: (n) => `${n}件の委員会報告が本会議討論に予定`,
govProposals: '政府提案',
govProposalsDesc: (n) => `${n}件の新しい政府提案が審議中`,
weekAhead: '来週の展望', committeeReportsTag: '委員会報告',
govPropsTag: '政府提案', oppMotionsTag: '野党動議',
// Enhanced summary labels
committeeReport: '委員会報告',
on: 'について',
governmentProposition: '政府提案',
regarding: 'に関する',
referredTo: 'に付託',
motionBy: '動議提出者',
parliamentaryMotion: '議会動議',
unknown: '不明',
// Analytical narrative labels
reportsOverview: (n) => `スウェーデン国会の委員会が${n}件の新しい報告書を発表し、現在進行中の立法作業を反映しています。`,
reportSignificance: 'この報告書は',
readFullReport: '報告書全文を読む',
propsOverview: (n) => `政府は議会に${n}件の新しい提案を提出しました。`,
propSignificance: 'この提案は',
readFullProp: '提案全文を読む',
motionsOverview: (n) => `野党議員が${n}件の新しい動議を提出しました。`,
motionSignificance: 'この動議は',
readFullMotion: '動議全文を読む',
policyContext: '政策的背景',
filedBy: '提出者'
},
ko: {
whyMatters: '이번 주가 중요한 이유',
whyMattersDefault: '이번 주에는 주요 토론, 위원회 회의 및 정부 협의를 포함한 중요한 의회 활동이 있습니다.',
keyEvents: '이번 주 주요 일정',
whatToWatch: '주목할 사항',
latestReports: '최신 위원회 보고서',
noReports: '현재 이용 가능한 위원회 보고서가 없습니다.',
committee: '위원회', document: '문서',
reportDefault: '의회 사안에 대한 위원회 보고서.',
govProps: '정부 법안',
noProps: '현재 이용 가능한 정부 법안이 없습니다.',
propDefault: '정부의 의회 법안.',
oppMotions: '야당 동의',
noMotions: '현재 이용 가능한 야당 동의가 없습니다.',
author: '저자', party: '정당',
motionDefault: '야당 의원의 의회 동의.',
genericContent: '콘텐츠 생성 중.',
monitorDev: '동향 및 결과 모니터링',
committeeDebates: '위원회 토론',
committeeDebatesDesc: (n) => `${n}개 위원회 보고서가 본회의 토론에 예정`,
govProposals: '정부 법안',
govProposalsDesc: (n) => `${n}개 새 정부 법안 검토 중`,
weekAhead: '다음 주 전망', committeeReportsTag: '위원회 보고서',
govPropsTag: '정부 법안', oppMotionsTag: '야당 동의',
// Enhanced summary labels
committeeReport: '위원회 보고서',
on: '에 관한',
governmentProposition: '정부 법안',
regarding: '에 관하여',
referredTo: '에 회부',
motionBy: '동의 제안자',
parliamentaryMotion: '의회 동의',
unknown: '알 수 없음',
// Analytical narrative labels
reportsOverview: (n) => `스웨덴 의회 위원회가 진행 중인 입법 작업을 반영하는 ${n}개의 새 보고서를 발표했습니다.`,
reportSignificance: '이 보고서는',
readFullReport: '전체 보고서 읽기',
propsOverview: (n) => `정부가 의회에 ${n}개의 새 법안을 제출했습니다.`,
propSignificance: '이 법안은',
readFullProp: '전체 법안 읽기',
motionsOverview: (n) => `야당 의원들이 ${n}개의 새 동의안을 제출했습니다.`,
motionSignificance: '이 동의안은',
readFullMotion: '전체 동의안 읽기',
policyContext: '정책 맥락',
filedBy: '제출자'
},
zh: {
whyMatters: '为什么本周很重要',
whyMattersDefault: '本周有重要的议会活动,包括关键辩论、委员会会议和政府磋商。',
keyEvents: '本周重要事件',
whatToWatch: '值得关注的要点',
latestReports: '最新委员会报告',
noReports: '目前没有可用的委员会报告。',
committee: '委员会', document: '文件',
reportDefault: '关于议会事务的委员会报告。',
govProps: '政府提案',
noProps: '目前没有可用的政府提案。',
propDefault: '政府向议会提交的提案。',
oppMotions: '反对党动议',
noMotions: '目前没有可用的反对党动议。',
author: '作者', party: '政党',
motionDefault: '反对党议员的议会动议。',
genericContent: '内容生成中。',
monitorDev: '监测发展动态和结果',
committeeDebates: '委员会辩论',
committeeDebatesDesc: (n) => `${n}份委员会报告安排在全体会议上辩论`,
govProposals: '政府提案',
govProposalsDesc: (n) => `${n}项新政府提案正在审查中`,
weekAhead: '下周展望', committeeReportsTag: '委员会报告',
govPropsTag: '政府提案', oppMotionsTag: '反对党动议',
// Enhanced summary labels
committeeReport: '委员会报告',
on: '关于',
governmentProposition: '政府提案',
regarding: '关于',
referredTo: '提交至',
motionBy: '动议提出者',
parliamentaryMotion: '议会动议',
unknown: '未知',
// Analytical narrative labels
reportsOverview: (n) => `瑞典国会各委员会发布了${n}份新报告,反映了正在进行的立法工作。`,
reportSignificance: '该报告涉及',
readFullReport: '阅读完整报告',
propsOverview: (n) => `政府向议会提交了${n}项新提案。`,
propSignificance: '该提案涉及',
readFullProp: '阅读完整提案',
motionsOverview: (n) => `反对党议员提交了${n}项新动议。`,
motionSignificance: '该动议涉及',
readFullMotion: '阅读完整动议',
policyContext: '政策背景',
filedBy: '提交者'
}
};
/**
* Get localized label with fallback to English
*/
export function L(lang, key) {
return CONTENT_LABELS[lang]?.[key] || CONTENT_LABELS.en[key];
}
function generateWeekAheadContent(data, lang) {
const { events, highlights, context } = data;
let content = '';
// Introduction section
content += `
<div class="context-box">
<h3>${L(lang, 'whyMatters')}</h3>
<p>${context || L(lang, 'whyMattersDefault')}</p>
</div>
`;
// Group events by significance
const highPriority = events.filter(e => isHighPriority(e));
if (highPriority.length > 0) {
content += `\n <h2>${L(lang, 'keyEvents')}</h2>\n`;
highPriority.forEach(event => {
// Derive dayName from event date if not present
const dayName = event.dayName || (event.datum || event.from || event.start ? formatDayName(new Date(event.datum || event.from || event.start), lang) : '');
const eventTime = event.time || event.tid || 'Expected';
const eventTitle = event.title || event.titel || 'Event';
// Mark Swedish API titles for LLM translation post-processing
const escapedEventTitle = escapeHtml(eventTitle);
const titleHtml = (event.titel && !event.title)
? `<span data-translate="true" lang="sv">${escapedEventTitle}</span>`
: escapedEventTitle;
content += `
<h3>${dayName ? dayName + ' - ' : ''}${titleHtml}</h3>
<p>${event.description || `${eventTime}: ${event.details || 'Parliamentary session scheduled.'}`}</p>
`;
});
}
// Additional context
if (highlights && highlights.length > 0) {
content += `\n <h2>${L(lang, 'whatToWatch')}</h2>\n <ul>\n`;
highlights.forEach(highlight => {
content += ` <li><strong>${highlight.title}:</strong> ${highlight.description}</li>\n`;
});
content += ' </ul>\n';
}
return content;
}
/**
* Determine if event is high priority
*/
function isHighPriority(event) {
const title = (event.title || event.rubrik || '').toLowerCase();
return (
title.includes('pm') ||
title.includes('prime minister') ||
title.includes('statsminister') ||
title.includes('vote') ||
title.includes('votering') ||
title.includes('eu') ||
title.includes('summit')
);
}
/**
* Generate enhanced summary from document metadata when summary field is missing
* Uses document type, subtype, organ, and other metadata to create informative placeholder
*
* @param {Object} doc - Document object
* @param {string} type - Document type (report, proposition, motion)
* @param {string} lang - Language code
* @returns {string} Enhanced summary text
*/
function generateEnhancedSummary(doc, type, lang) {
// If we have a real summary or notis, use it
if (doc.summary || doc.notis) {
return doc.summary || doc.notis;
}
// Generate enhanced summary based on metadata
const organ = doc.organ || doc.committee;
const subtyp = doc.subtyp || doc.subtype;
const doktyp = doc.doktyp || doc.documentType;
// Build contextual summary based on available metadata
const parts = [];
if (type === 'report' && organ) {
parts.push(`${organ} ${L(lang, 'committeeReport')}`);
if (subtyp) parts.push(`${L(lang, 'on')} ${subtyp}`);
} else if (type === 'proposition') {
parts.push(L(lang, 'governmentProposition'));
if (subtyp) parts.push(`${L(lang, 'regarding')} ${subtyp}`);
if (organ) parts.push(`${L(lang, 'referredTo')} ${organ}`);
} else if (type === 'motion') {
const author = doc.intressent_namn || doc.author;
const party = doc.parti;
if (author && party) {
parts.push(`${L(lang, 'motionBy')} ${author} (${party})`);
} else if (author) {
parts.push(`${L(lang, 'motionBy')} ${author}`);
} else {
parts.push(L(lang, 'parliamentaryMotion'));
}
if (subtyp) parts.push(`${L(lang, 'on')} ${subtyp}`);
}
// Add document type information if useful
if (doktyp && doktyp !== type) {
parts.push(`(${doktyp})`);
}
// Fallback to default if no useful metadata
if (parts.length === 0) {
return type === 'report' ? L(lang, 'reportDefault') :
type === 'proposition' ? L(lang, 'propDefault') :
L(lang, 'motionDefault');
}
return parts.join(' ') + '.';
}
/**
* Map Swedish committee codes to full names for richer descriptions
*/
const COMMITTEE_NAMES = {
AU: { en: 'Labour Market Committee', sv: 'Arbetsmarknadsutskottet' },
CU: { en: 'Civil Affairs Committee', sv: 'Civilutskottet' },
FiU: { en: 'Finance Committee', sv: 'Finansutskottet' },
FöU: { en: 'Defence Committee', sv: 'Försvarsutskottet' },
JuU: { en: 'Justice Committee', sv: 'Justitieutskottet' },
KU: { en: 'Constitutional Committee', sv: 'Konstitutionsutskottet' },
KrU: { en: 'Cultural Affairs Committee', sv: 'Kulturutskottet' },
MJU: { en: 'Environment and Agriculture Committee', sv: 'Miljö- och jordbruksutskottet' },
NU: { en: 'Industry and Trade Committee', sv: 'Näringsutskottet' },
SkU: { en: 'Taxation Committee', sv: 'Skatteutskottet' },
SfU: { en: 'Social Insurance Committee', sv: 'Socialförsäkringsutskottet' },
SoU: { en: 'Social Committee', sv: 'Socialutskottet' },
TU: { en: 'Transport Committee', sv: 'Trafikutskottet' },
UbU: { en: 'Education Committee', sv: 'Utbildningsutskottet' },
UU: { en: 'Foreign Affairs Committee', sv: 'Utrikesutskottet' },
};
/**
* Get human-readable committee name from code
*/
function getCommitteeName(code, lang) {
if (!code) return L(lang, 'unknown');
const entry = COMMITTEE_NAMES[code];
if (!entry) return code;
// Use Swedish name for sv, English for all others (other languages get translated via data-translate)
return lang === 'sv' ? entry.sv : entry.en;
}
/**
* Generate Committee Reports content with analytical narrative
*/
function generateCommitteeContent(data, lang) {
const reports = data.reports || [];
let content = `<h2>${L(lang, 'latestReports')}</h2>\n`;
if (reports.length === 0) {
content += `<p>${L(lang, 'noReports')}</p>\n`;
return content;
}
// Lede paragraph: overview of committee activity
const overviewFn = L(lang, 'reportsOverview');
const overviewText = typeof overviewFn === 'function' ? overviewFn(reports.length) : `${reports.length} new committee reports published.`;
content += `<p class="article-lede">${escapeHtml(overviewText)}</p>\n`;
// Group reports by committee for thematic coherence
const byCommittee = {};
reports.forEach(report => {
const committee = report.organ || report.committee || 'other';
if (!byCommittee[committee]) byCommittee[committee] = [];
byCommittee[committee].push(report);
});
// Generate content for each committee group
Object.entries(byCommittee).forEach(([committeeCode, committeeReports]) => {
const committeeName = getCommitteeName(committeeCode, lang);
// Committee section header (only if multiple committees)
if (Object.keys(byCommittee).length > 1) {
content += `\n <h3>${escapeHtml(committeeName)}</h3>\n`;
}
committeeReports.forEach(report => {
const titleText = report.titel || report.title || '';
const escapedTitle = escapeHtml(titleText);
const titleHtml = (report.titel && !report.title)
? `<span data-translate="true" lang="sv">${escapedTitle}</span>`
: escapedTitle;
const docName = escapeHtml(report.dokumentnamn || report.dok_id || titleText);
// Use enriched summary or enhanced summary from metadata
const summaryText = generateEnhancedSummary(report, 'report', lang);
const isFromAPI = report.summary || report.notis;
const summaryHtml = (report.titel && !report.title && isFromAPI && summaryText !== L(lang, 'reportDefault'))
? `<span data-translate="true" lang="sv">${escapeHtml(summaryText)}</span>`
: escapeHtml(summaryText);
// Build a narrative paragraph instead of bare fields
if (Object.keys(byCommittee).length > 1) {
// Sub-item under committee header
content += `
<div class="report-entry">
<h4>${titleHtml}</h4>
<p>${escapeHtml(L(lang, 'reportSignificance'))} ${summaryHtml}</p>
<p><a href="${report.url}" class="document-link" rel="noopener noreferrer">${escapeHtml(L(lang, 'readFullReport'))}: ${docName}</a></p>
</div>
`;
} else {
// Single committee - use h3 for each report
content += `
<div class="report-entry">
<h3>${titleHtml}</h3>
<p><strong>${L(lang, 'committee')}:</strong> ${escapeHtml(committeeName)}</p>
<p>${escapeHtml(L(lang, 'reportSignificance'))} ${summaryHtml}</p>
<p><a href="${report.url}" class="document-link" rel="noopener noreferrer">${escapeHtml(L(lang, 'readFullReport'))}: ${docName}</a></p>
</div>
`;
}
});
});
return content;
}
/**
* Generate Propositions content with analytical narrative
*/
function generatePropositionsContent(data, lang) {
const propositions = data.propositions || [];
let content = `<h2>${L(lang, 'govProps')}</h2>\n`;
if (propositions.length === 0) {
content += `<p>${L(lang, 'noProps')}</p>\n`;
return content;
}
// Lede paragraph: overview of government legislative activity
const overviewFn = L(lang, 'propsOverview');
const overviewText = typeof overviewFn === 'function' ? overviewFn(propositions.length) : `${propositions.length} new government propositions submitted.`;
content += `<p class="article-lede">${escapeHtml(overviewText)}</p>\n`;
propositions.forEach(prop => {
const titleText = prop.titel || prop.title || '';
const escapedTitle = escapeHtml(titleText);
const titleHtml = (prop.titel && !prop.title)
? `<span data-translate="true" lang="sv">${escapedTitle}</span>`
: escapedTitle;
const docName = escapeHtml(prop.dokumentnamn || prop.dok_id || titleText);
// Use enhanced summary based on metadata
const summaryText = generateEnhancedSummary(prop, 'proposition', lang);
const isFromAPI = prop.summary || prop.notis;
const summaryHtml = (prop.titel && !prop.title && isFromAPI && summaryText !== L(lang, 'propDefault'))
? `<span data-translate="true" lang="sv">${escapeHtml(summaryText)}</span>`
: escapeHtml(summaryText);
// Committee the proposition is referred to
const referredCommittee = prop.organ || prop.committee;
const referredLine = referredCommittee
? `<br><strong>${L(lang, 'referredTo')}:</strong> ${escapeHtml(getCommitteeName(referredCommittee, lang))}`
: '';
content += `
<div class="proposition-entry">
<h3>${titleHtml}</h3>
<p>${escapeHtml(L(lang, 'propSignificance'))} ${summaryHtml}${referredLine}</p>
<p><a href="${prop.url}" class="document-link" rel="noopener noreferrer">${escapeHtml(L(lang, 'readFullProp'))}: ${docName}</a></p>
</div>
`;
});
return content;
}
/**
* Generate Motions content with analytical narrative
*/
function generateMotionsContent(data, lang) {
const motions = data.motions || [];
let content = `<h2>${L(lang, 'oppMotions')}</h2>\n`;
if (motions.length === 0) {
content += `<p>${L(lang, 'noMotions')}</p>\n`;
return content;
}
// Lede paragraph: overview of opposition activity
const overviewFn = L(lang, 'motionsOverview');
const overviewText = typeof overviewFn === 'function' ? overviewFn(motions.length) : `${motions.length} new opposition motions filed.`;
content += `<p class="article-lede">${escapeHtml(overviewText)}</p>\n`;
motions.forEach(motion => {
const titleText = motion.titel || motion.title || '';
const escapedTitle = escapeHtml(titleText);
const titleHtml = (motion.titel && !motion.title)
? `<span data-translate="true" lang="sv">${escapedTitle}</span>`
: escapedTitle;
const docName = escapeHtml(motion.dokumentnamn || motion.dok_id || titleText);
// Use enriched author and party data
const authorName = motion.intressent_namn || motion.author || L(lang, 'unknown');
const partyName = motion.parti || '';
const authorLine = partyName
? `${escapeHtml(authorName)} (${escapeHtml(partyName)})`
: escapeHtml(authorName);
// Use enhanced summary based on metadata
const summaryText = generateEnhancedSummary(motion, 'motion', lang);
const isFromAPI = motion.summary || motion.notis;
const summaryHtml = (motion.titel && !motion.title && isFromAPI && summaryText !== L(lang, 'motionDefault'))
? `<span data-translate="true" lang="sv">${escapeHtml(summaryText)}</span>`
: escapeHtml(summaryText);
content += `
<div class="motion-entry">
<h3>${titleHtml}</h3>
<p><strong>${L(lang, 'filedBy')}:</strong> ${authorLine}</p>
<p>${escapeHtml(L(lang, 'motionSignificance'))} ${summaryHtml}</p>
<p><a href="${motion.url}" class="document-link" rel="noopener noreferrer">${escapeHtml(L(lang, 'readFullMotion'))}: ${docName}</a></p>
</div>
`;
});
return content;
}
/**
* Generate generic content
*/
function generateGenericContent(data, lang) {
return `<p>${L(lang, 'genericContent')}</p>`;
}
/**
* Extract "Watch Points" from data
*
* @param {Object} data - MCP data
* @param {string} lang - Language code
* @returns {Array} Watch points for article
*/
export function extractWatchPoints(data, lang = 'en') {
const watchPoints = [];
// From calendar events
if (data.events) {
const highPriorityEvents = data.events.filter(isHighPriority);
highPriorityEvents.forEach(event => {
// Derive dayName from event date if not present
const dayName = event.dayName || (event.datum || event.from || event.start ? formatDayName(new Date((event.datum || event.from || event.start).split('T')[0]), lang) : '');
const eventTitle = event.title || event.titel || 'Event';
// Mark Swedish API titles for LLM translation post-processing
const escapedEventTitle = escapeHtml(eventTitle);
const titleDisplay = (event.titel && !event.title)
? `<span data-translate="true" lang="sv">${escapedEventTitle}</span>`
: escapedEventTitle;
watchPoints.push({
title: dayName ? `${dayName}: ${titleDisplay}` : titleDisplay,
description: event.description || L(lang, 'monitorDev')
});
});
}
// From committee reports
if (data.reports && data.reports.length > 0) {
watchPoints.push({
title: L(lang, 'committeeDebates'),
description: L(lang, 'committeeDebatesDesc')(data.reports.length)
});
}
// From propositions
if (data.propositions && data.propositions.length > 0) {
watchPoints.push({
title: L(lang, 'govProposals'),
description: L(lang, 'govProposalsDesc')(data.propositions.length)
});
}
return watchPoints.slice(0, 5); // Max 5 watch points
}
/**
* Generate article metadata
*
* @param {Object} data - Article data
* @param {string} type - Article type
* @param {string} lang - Language code
* @returns {Object} Article metadata
*/
export function generateMetadata(data, type, lang = 'en') {
const keywords = [];
const topics = [];
const tags = [];
// Add type-specific keywords
switch (type) {
case 'week-ahead':
keywords.push('parliament', 'week ahead', 'calendar', 'events');
topics.push('parliament');
tags.push(L(lang, 'weekAhead'));
break;
case 'committee-reports':
keywords.push('committee', 'reports', 'betänkanden', 'parliament');
topics.push('committees', 'reports');
tags.push(L(lang, 'committeeReportsTag'));
break;
case 'propositions':
keywords.push('government', 'propositions', 'parliament', 'legislation');
topics.push('government', 'legislation');
tags.push(L(lang, 'govPropsTag'));
break;
case 'motions':
keywords.push('motions', 'opposition', 'parliament', 'proposals');
topics.push('parliament', 'opposition');
tags.push(L(lang, 'oppMotionsTag'));
break;
}
// Extract additional keywords from data
if (data.events) {
keywords.push('calendar', 'events', 'debates');
}
if (data.reports) {
keywords.push('committees', 'reports');
}
// Add common keywords
keywords.push('Swedish Parliament', 'Riksdag', 'politics', 'Sweden');
return {
keywords: keywords.slice(0, 15),
topics: topics.slice(0, 5),
tags: tags.slice(0, 10)
};
}
/**
* Calculate estimated read time
*
* @param {string} content - Article HTML content
* @returns {string} Read time (e.g., "5 min read")
*/
export function calculateReadTime(content) {
// Remove HTML tags for word count
const text = content.replace(/<[^>]*>/g, ' ');
const words = text.trim().split(/\s+/).length;
// Average reading speed: 200 words per minute
const minutes = Math.ceil(words / 200);
return `${minutes} min read`;
}
/**
* Generate article sources list
*
* @param {Array} tools - MCP tools used
* @returns {Array} Sources list
*/
export function generateSources(tools = []) {
const sources = ['riksdag-regering-mcp'];
if (tools.includes('get_calendar_events')) {
sources.push('Riksdagen Calendar');
}
if (tools.includes('get_betankanden')) {
sources.push('Committee Reports');
}
if (tools.includes('get_propositioner')) {
sources.push('Government Propositions');
}
if (tools.includes('get_motioner')) {
sources.push('Parliamentary Motions');
}
if (tools.includes('search_dokument')) {
sources.push('Riksdagen Documents');
}
if (tools.includes('get_dokument_innehall')) {
sources.push('Riksdagen Document Content');
}
return sources;
}
export default {
transformCalendarToEventGrid,
generateArticleContent,
extractWatchPoints,
extractTopics,
generateMetadata,
calculateReadTime,
generateSources,
CONTENT_LABELS,
L
};