use super::super::helpers::localized_module_name;
use crate::audit::normalized::NormalizedReport;
use crate::output::report_model::{
AffectedElement, AppendixBlock, AppendixViolation, CapabilitySignal, MethodologyBlock,
};
pub(super) fn build_appendix_block_from_normalized(normalized: &NormalizedReport) -> AppendixBlock {
let violations: Vec<AppendixViolation> = normalized
.findings
.iter()
.map(|f| {
let affected_elements: Vec<AffectedElement> = f
.occurrences
.iter()
.map(|occ| AffectedElement {
selector: occ.selector.clone().unwrap_or_else(|| occ.node_id.clone()),
node_id: occ.node_id.clone(),
})
.collect();
AppendixViolation {
rule: f.wcag_criterion.clone(),
rule_name: f.title.clone(),
severity: f.severity,
message: f.description.clone(),
fix_suggestion: f.occurrences.first().and_then(|o| o.fix_suggestion.clone()),
affected_elements,
}
})
.collect();
let has_violations = !violations.is_empty();
AppendixBlock {
violations,
score_methodology: "Score-Berechnung: Basis 100 Punkte. Abzug auf Basis der Taxonomie-Regel-Definitionen \
mit regelspezifischen Penalties und logarithmischer Skalierung für wiederholte Verstöße. \
Konfligierende Signale werden als Hinweis markiert, verändern den Score jedoch nicht nachträglich."
.to_string(),
has_violations,
}
}
pub(super) fn build_methodology(
i18n: &crate::i18n::I18n,
normalized: &NormalizedReport,
) -> MethodologyBlock {
let locale = i18n.locale();
let en = locale == "en";
let active_modules = normalized
.module_scores
.iter()
.map(|m| m.name.clone())
.collect::<Vec<_>>()
.join(", ");
let scope = if en {
format!(
"Automated audit of {} for accessibility per WCAG 2.1 (level {}). \
Performance, SEO, security and mobile usability were also analyzed.",
normalized.url, normalized.wcag_level
)
} else {
format!(
"Automatisierte Prüfung der Seite {} auf Barrierefreiheit nach WCAG 2.1 (Level {}). \
Zusätzlich wurden Performance, SEO, Sicherheit und mobile Nutzbarkeit analysiert.",
normalized.url, normalized.wcag_level
)
};
let method = if en {
"The audit was performed via the Chrome DevTools Protocol (CDP) and the browser's native \
accessibility tree. 21 WCAG rules were checked automatically against the page content."
.to_string()
} else {
"Die Prüfung erfolgte über den Chrome DevTools Protocol (CDP) und den \
nativen Accessibility Tree des Browsers. 21 WCAG-Regeln wurden automatisiert \
gegen den Seiteninhalt geprüft."
.to_string()
};
let limitations = if en {
"Automated tests can detect about 30–40% of all accessibility issues. Complex aspects \
such as correct tab order, meaningful alt texts, or understandable language additionally \
require manual review."
.to_string()
} else {
"Automatisierte Tests können ca. 30–40% aller Barrierefreiheitsprobleme erkennen. \
Komplexe Aspekte wie korrekte Tab-Reihenfolge, sinnvolle Alt-Texte oder \
verständliche Sprache erfordern zusätzlich manuelle Prüfung."
.to_string()
};
let disclaimer = if en {
"This report represents an automated technical analysis. It does not replace a complete \
WCAG 2.1 conformance assessment. A legally defensible accessibility statement requires a \
comprehensive manual audit by experts."
.to_string()
} else {
"Dieser Report stellt eine automatisierte technische Analyse dar. \
Er ersetzt keine vollständige Konformitätsbewertung nach WCAG 2.1. \
Für eine rechtsverbindliche Aussage zur Barrierefreiheit ist eine \
umfassende manuelle Prüfung durch Experten erforderlich."
.to_string()
};
let key = |de: &str, en_label: &str| -> String {
if en {
en_label.to_string()
} else {
de.to_string()
}
};
let preview_value = if normalized.has_screenshots {
if en {
"Desktop and mobile captured".to_string()
} else {
"Desktop und Mobile erfasst".to_string()
}
} else if en {
"Not captured".to_string()
} else {
"Nicht erfasst".to_string()
};
let total_score_value = {
let contributing: Vec<_> = normalized
.module_scores
.iter()
.filter(|m| m.contributes_to_overall)
.collect();
let total_raw: u32 = contributing.iter().map(|m| m.weight_pct).sum();
let weights: Vec<String> = contributing
.iter()
.map(|m| {
let pct = (m.weight_pct * 100 + total_raw / 2)
.checked_div(total_raw)
.unwrap_or(0);
let name = localized_module_name(&m.name, i18n);
if en {
format!("{name} {pct}%")
} else {
format!("{name} {pct} %")
}
})
.collect();
let weights_label = if weights.is_empty() {
if en {
"Accessibility 100%".to_string()
} else {
"Barrierefreiheit 100 %".to_string()
}
} else {
weights.join(", ")
};
let indicator_names: Vec<String> = normalized
.module_scores
.iter()
.filter(|m| !m.contributes_to_overall || m.measurement_type == "heuristic")
.map(|m| localized_module_name(&m.name, i18n))
.collect();
let indicator_note = if indicator_names.is_empty() {
String::new()
} else if en {
format!(
" Indicator modules shown separately: {}.",
indicator_names.join(", ")
)
} else {
format!(
" Separat ausgewiesene Indikator-Module: {}.",
indicator_names.join(", ")
)
};
if en {
format!(
"{} / 100 — contributing weights: {}{}",
normalized.overall_score, weights_label, indicator_note
)
} else {
format!(
"{} / 100 — beitragende Gewichtung: {}{}",
normalized.overall_score, weights_label, indicator_note
)
}
};
let runtime_unit = "s";
MethodologyBlock {
scope,
method,
limitations,
disclaimer,
audit_facts: vec![
(
key("Primärscore", "Primary score"),
format!("Accessibility {} / 100", normalized.score),
),
(key("Gesamtscore", "Overall score"), total_score_value),
(
key("WCAG-Level", "WCAG level"),
normalized.wcag_level.to_string(),
),
(
key("Geprüfte Knoten", "Checked nodes"),
normalized.nodes_analyzed.to_string(),
),
(
key("Laufzeit", "Runtime"),
format!(
"{:.1} {}",
normalized.duration_ms as f64 / 1000.0,
runtime_unit
),
),
(key("Aktive Module", "Active modules"), active_modules),
(
key("Audit-Hinweise", "Audit notes"),
normalized.audit_flags.len().to_string(),
),
(key("Vorschau", "Preview"), preview_value),
],
confidence_summary: build_confidence_summary(locale, normalized),
capabilities: build_capability_matrix(locale, normalized),
}
}
fn build_confidence_summary(locale: &str, normalized: &NormalizedReport) -> Vec<(String, String)> {
let en = locale == "en";
let base_confidence = if normalized.nodes_analyzed >= 2_000 {
if en {
"High"
} else {
"Hoch"
}
} else if normalized.nodes_analyzed >= 500 {
if en {
"Solid"
} else {
"Solide"
}
} else if en {
"Limited"
} else {
"Begrenzt"
};
let caveat_level = if normalized.audit_flags.is_empty() {
if en {
"No automatically detected conflict signals"
} else {
"Keine automatisiert erkannten Konfliktsignale"
}
} else if normalized.audit_flags.len() == 1 {
if en {
"1 caveat signal"
} else {
"1 Hinweissignal"
}
} else if en {
"Multiple caveat signals"
} else {
"Mehrere Hinweissignale"
};
let module_coverage = if normalized.module_scores.len() >= 5 {
if en {
"Broad"
} else {
"Breit"
}
} else if normalized.module_scores.len() >= 3 {
if en {
"Extended"
} else {
"Erweitert"
}
} else if en {
"Core checks"
} else {
"Kern-Checks"
};
let (label_trust, label_coverage, label_signals, label_manual, val_manual) = if en {
(
"Audit confidence",
"Module coverage",
"Conflict signals",
"Manual review needed",
"Yes, for semantic quality and usage context",
)
} else {
(
"Audit-Vertrauen",
"Modul-Abdeckung",
"Konfliktsignale",
"Manuelle Prüfung nötig",
"Ja, für semantische Qualität und Nutzungskontext",
)
};
vec![
(label_trust.to_string(), base_confidence.to_string()),
(label_coverage.to_string(), module_coverage.to_string()),
(label_signals.to_string(), caveat_level.to_string()),
(label_manual.to_string(), val_manual.to_string()),
]
}
fn build_capability_matrix(locale: &str, normalized: &NormalizedReport) -> Vec<CapabilitySignal> {
let en = locale == "en";
let confidence_high = if en { "High" } else { "Hoch" };
let confidence_solid = if en { "Solid" } else { "Solide" };
let confidence_off = if en { "Not active" } else { "Nicht aktiv" };
let mut capabilities = vec![
CapabilitySignal {
signal: if en {
"WCAG rules & occurrences".into()
} else {
"WCAG-Regeln & Vorkommen".into()
},
source: if en {
"Accessibility tree + rule engine".into()
} else {
"Accessibility Tree + Regelengine".into()
},
confidence: if normalized.nodes_analyzed >= 500 {
confidence_high.to_string()
} else {
confidence_solid.to_string()
},
surfaces: vec!["CLI".into(), "JSON".into(), "PDF".into(), "Studio".into()],
note: if en {
"Primary audit truth for automatically detectable violations.".to_string()
} else {
"Primäre Audit-Wahrheit für automatisiert erkennbare Verstöße.".to_string()
},
},
CapabilitySignal {
signal: if en {
"Web vitals & loading indicators".into()
} else {
"Web Vitals & Ladeindikatoren".into()
},
source: if en {
"Performance module".into()
} else {
"Performance-Modul".into()
},
confidence: if normalized
.module_scores
.iter()
.any(|m| m.name == "Performance")
{
confidence_high.to_string()
} else {
confidence_off.to_string()
},
surfaces: vec!["CLI".into(), "JSON".into(), "PDF".into()],
note: if en {
"FCP, CLS and TTFB are reflected in facts and module sections.".to_string()
} else {
"FCP, CLS und TTFB werden in Facts und Modulkapiteln gespiegelt.".to_string()
},
},
CapabilitySignal {
signal: if en {
"SEO structure & schema".into()
} else {
"SEO-Struktur & Schema".into()
},
source: if en {
"SEO module".into()
} else {
"SEO-Modul".into()
},
confidence: if normalized.module_scores.iter().any(|m| m.name == "SEO") {
confidence_solid.to_string()
} else {
confidence_off.to_string()
},
surfaces: vec!["CLI".into(), "JSON".into(), "PDF".into()],
note: if en {
"Meta, heading and schema signals are condensed into a report-ready form."
.to_string()
} else {
"Meta-, Heading- und Schema-Signale sind reportfähig verdichtet.".to_string()
},
},
CapabilitySignal {
signal: if en {
"Security headers & HTTPS".into()
} else {
"Security Header & HTTPS".into()
},
source: if en {
"Security module".into()
} else {
"Security-Modul".into()
},
confidence: if normalized
.module_scores
.iter()
.any(|m| m.name == "Security")
{
confidence_high.to_string()
} else {
confidence_off.to_string()
},
surfaces: vec!["CLI".into(), "JSON".into(), "PDF".into()],
note: if en {
"Header presence and HTTPS status remain visible as raw signal.".to_string()
} else {
"Header-Präsenz und HTTPS-Status bleiben als Rohsignal sichtbar.".to_string()
},
},
CapabilitySignal {
signal: if en {
"Mobile, UX, journey".into()
} else {
"Mobile, UX, Journey".into()
},
source: if en {
"Heuristic modules".into()
} else {
"Heuristik-Module".into()
},
confidence: if en {
"Indicator-based".into()
} else {
"Hinweisbasiert".into()
},
surfaces: vec!["CLI".into(), "JSON".into(), "PDF".into(), "Studio".into()],
note: if en {
"Useful for prioritization, not the sole UX truth.".to_string()
} else {
"Zur Priorisierung geeignet, nicht als alleinige UX-Gesamtwahrheit.".to_string()
},
},
];
if !normalized.audit_flags.is_empty() {
capabilities.push(CapabilitySignal {
signal: if en {
"Audit conflict signals".into()
} else {
"Audit-Konfliktsignale".into()
},
source: if en {
"Normalization / cross-checks".into()
} else {
"Normalisierung / Cross-Checks".into()
},
confidence: if en {
"Explicitly flagged".into()
} else {
"Explizit markiert".into()
},
surfaces: vec!["JSON".into(), "PDF".into()],
note: if en {
format!(
"{} conflict signal(s) are surfaced openly rather than hidden in the score.",
normalized.audit_flags.len()
)
} else {
format!(
"{} Konfliktsignal(e) werden offen ausgewiesen statt im Score versteckt.",
normalized.audit_flags.len()
)
},
});
}
capabilities
}