auditmysite 0.26.0

Resource-efficient WCAG 2.1 Accessibility Checker in Rust
Documentation
use crate::audit::normalized::NormalizedReport;
use crate::output::report_model::{ModuleScore, ModulesBlock};

use super::super::helpers::{interpret_score, localized_module_name, InterpretArea};
use super::super::modules::{
    derive_accessibility_card_context, derive_accessibility_context, derive_accessibility_lever,
    derive_mobile_card_context, derive_mobile_context, derive_mobile_lever,
    derive_performance_card_context, derive_performance_context, derive_performance_lever,
    derive_security_card_context, derive_security_context, derive_security_lever,
    derive_seo_card_context, derive_seo_context, derive_seo_lever,
};
use super::super::seo::build_seo_interpretation;
use super::module_details::normalized_module_score;

pub(super) fn build_modules_block_from_normalized(
    locale: &str,
    normalized: &NormalizedReport,
) -> ModulesBlock {
    let en = locale == "en";
    let a11y_score = normalized.score as f32;
    let mut dashboard = vec![ModuleScore {
        name: if en {
            "Accessibility".into()
        } else {
            "Barrierefreiheit".into()
        },
        score: a11y_score.round() as u32,
        measurement_type: "measured".into(),
        interpretation: interpret_score(InterpretArea::Accessibility, a11y_score, locale),
        card_context: derive_accessibility_card_context(locale, normalized),
        score_context: derive_accessibility_context(locale, normalized),
        key_lever: derive_accessibility_lever(locale, normalized),
        good_threshold: 75,
        warn_threshold: 50,
    }];

    if let Some(ref p) = normalized.raw_performance {
        let score = normalized_module_score(normalized, "Performance").unwrap_or(p.score.overall);
        dashboard.push(ModuleScore {
            name: "Performance".into(),
            score,
            measurement_type: "measured".into(),
            interpretation: interpret_score(InterpretArea::Performance, score as f32, locale),
            card_context: derive_performance_card_context(locale, p),
            score_context: derive_performance_context(locale, p),
            key_lever: derive_performance_lever(locale, p),
            good_threshold: 75,
            warn_threshold: 50,
        });
    }
    if let Some(ref s) = normalized.raw_seo {
        let score = normalized_module_score(normalized, "SEO").unwrap_or(s.score);
        dashboard.push(ModuleScore {
            name: "SEO".into(),
            score,
            measurement_type: "measured".into(),
            interpretation: build_seo_interpretation(locale, s),
            card_context: derive_seo_card_context(locale, s),
            score_context: derive_seo_context(locale, s),
            key_lever: derive_seo_lever(locale, s),
            good_threshold: 75,
            warn_threshold: 50,
        });
    }
    if let Some(ref s) = normalized.raw_security {
        let score = normalized_module_score(normalized, "Security").unwrap_or(s.score);
        dashboard.push(ModuleScore {
            name: if en {
                "Security".into()
            } else {
                "Sicherheit".into()
            },
            score,
            measurement_type: "measured".into(),
            interpretation: interpret_score(InterpretArea::Security, score as f32, locale),
            card_context: derive_security_card_context(locale, s),
            score_context: derive_security_context(locale, s),
            key_lever: derive_security_lever(locale, s),
            good_threshold: 75,
            warn_threshold: 50,
        });
    }
    if let Some(ref m) = normalized.raw_mobile {
        let score = normalized_module_score(normalized, "Mobile").unwrap_or(m.score);
        dashboard.push(ModuleScore {
            name: "Mobile".into(),
            score,
            measurement_type: "measured".into(),
            interpretation: interpret_score(InterpretArea::Mobile, score as f32, locale),
            card_context: derive_mobile_card_context(locale, m),
            score_context: derive_mobile_context(locale, m),
            key_lever: derive_mobile_lever(locale, m),
            good_threshold: 75,
            warn_threshold: 50,
        });
    }
    if let Some(ref u) = normalized.raw_ux {
        let ux_score = normalized_module_score(normalized, "UX").unwrap_or(u.score);
        let ux_context = format!(
            "CTA Clarity {}/100, Visual Hierarchy {}/100, Content Clarity {}/100, Trust Signals {}/100, Cognitive Load {}/100",
            u.cta_clarity.score, u.visual_hierarchy.score, u.content_clarity.score, u.trust_signals.score, u.cognitive_load.score
        );
        let ux_lever: String = if u.cta_clarity.score < 60 {
            if en {
                "Phrase CTA texts more clearly and specifically"
            } else {
                "CTA-Texte klarer und spezifischer formulieren"
            }
        } else if u.trust_signals.score < 60 {
            if en {
                "Add trust signals (contact, imprint)"
            } else {
                "Vertrauenssignale (Kontakt, Impressum) ergänzen"
            }
        } else if u.visual_hierarchy.score < 60 {
            if en {
                "Clean up heading structure (H1 → H2 → H3)"
            } else {
                "Heading-Struktur bereinigen (H1 → H2 → H3)"
            }
        } else if en {
            "Maintain UX quality at the current good level"
        } else {
            "UX-Qualität auf gutem Niveau halten"
        }
        .into();
        dashboard.push(ModuleScore {
            name: "UX".into(),
            score: ux_score,
            measurement_type: "heuristic".into(),
            interpretation: interpret_score(InterpretArea::Ux, ux_score as f32, locale),
            card_context: ux_context.clone(),
            score_context: ux_context,
            key_lever: ux_lever,
            good_threshold: 80,
            warn_threshold: 55,
        });
    }
    if let Some(ref j) = normalized.raw_journey {
        let journey_score = normalized_module_score(normalized, "Journey").unwrap_or(j.score);
        let journey_context = format!(
            "{}: {} · Entry {}/100, Orientation {}/100, Navigation {}/100, Interaction {}/100, Conversion {}/100",
            if en { "Intent" } else { "Seitenabsicht" },
            j.page_intent.label(),
            j.entry_clarity.score,
            j.orientation.score,
            j.navigation.score,
            j.interaction.score,
            j.conversion.score
        );
        let journey_lever = j
            .friction_points
            .first()
            .map(|fp| fp.recommendation.clone())
            .unwrap_or_else(|| {
                if en {
                    "Maintain journey clarity at the current level".to_string()
                } else {
                    "Journey-Klarheit auf aktuellem Niveau halten".to_string()
                }
            });
        dashboard.push(ModuleScore {
            name: "Journey".into(),
            score: journey_score,
            measurement_type: "heuristic".into(),
            interpretation: interpret_score(InterpretArea::Journey, journey_score as f32, locale),
            card_context: journey_context.clone(),
            score_context: journey_context,
            key_lever: journey_lever,
            good_threshold: 80,
            warn_threshold: 55,
        });
    }

    let has_multiple = dashboard.len() > 1;
    let overall_score = if has_multiple {
        Some(normalized.overall_score)
    } else {
        None
    };
    let overall_interpretation =
        overall_score.map(|_| build_overall_score_explanation(locale, normalized));

    ModulesBlock {
        dashboard,
        overall_score,
        overall_interpretation,
    }
}

fn build_overall_score_explanation(locale: &str, normalized: &NormalizedReport) -> String {
    let en = locale == "en";
    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, en))
        .collect();
    let indicator_note = if indicator_names.is_empty() {
        String::new()
    } else if en {
        format!(
            " Indicator modules ({}) are shown separately and do not change the overall score.",
            indicator_names.join(", ")
        )
    } else {
        format!(
            " Indikator-Module ({}) werden separat ausgewiesen und verändern den Gesamtscore nicht.",
            indicator_names.join(", ")
        )
    };

    if normalized.viewport_scores.is_some() {
        if en {
            format!(
                "Overall score uses the dual-viewport result: 70% mobile and 30% desktop. \
                 Security contributes 10% when active.{indicator_note}"
            )
        } else {
            format!(
                "Der Gesamtscore nutzt das Dual-Viewport-Ergebnis: 70% Mobile und 30% Desktop. \
                 Sicherheit fließt mit 10% ein, wenn aktiv.{indicator_note}"
            )
        }
    } else {
        let contributing: Vec<String> = normalized
            .module_scores
            .iter()
            .filter(|m| m.contributes_to_overall)
            .map(|m| {
                let name = localized_module_name(&m.name, en);
                if en {
                    format!("{name} {}%", m.weight_pct)
                } else {
                    format!("{name} {} %", m.weight_pct)
                }
            })
            .collect();
        let weights = if contributing.is_empty() {
            if en {
                "Accessibility 100%".to_string()
            } else {
                "Barrierefreiheit 100 %".to_string()
            }
        } else {
            contributing.join(", ")
        };
        if en {
            format!("The overall rating is the weighted result of the assessed areas: {weights}.{indicator_note}")
        } else {
            format!("Die Gesamtbewertung ergibt sich aus den gewichteten Einzelmodulen: {weights}.{indicator_note}")
        }
    }
}