use crate::audit::PerformanceResults;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PerfCriticalQualifier {
HighDomComplexity(i64),
LateThrottledLcp(f64),
RenderBlocking(usize),
UnminifiedAssets(f64),
NonCompositedAnimations(u32),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PerfPositiveQualifier {
LayoutStability,
MainThreadBlocking,
}
pub fn derive_performance_qualifiers(
p: &PerformanceResults,
throttled_lcp_max: f64,
) -> (Vec<PerfCriticalQualifier>, Vec<PerfPositiveQualifier>) {
let mut critical = Vec::new();
if let Some(nodes) = p.vitals.dom_nodes.filter(|n| *n > 3000) {
critical.push(PerfCriticalQualifier::HighDomComplexity(nodes));
}
if throttled_lcp_max > 4000.0 {
critical.push(PerfCriticalQualifier::LateThrottledLcp(throttled_lcp_max));
}
let rb_count = p
.render_blocking
.as_ref()
.map(|rb| rb.blocking_scripts.len() + rb.blocking_css.len())
.unwrap_or(0);
if rb_count > 0 {
critical.push(PerfCriticalQualifier::RenderBlocking(rb_count));
}
let unminified_kb = p
.minification
.as_ref()
.map(|m| m.total_savings_bytes as f64 / 1024.0)
.unwrap_or(0.0);
if unminified_kb > 100.0 {
critical.push(PerfCriticalQualifier::UnminifiedAssets(unminified_kb));
}
let anim_count = p.animations.as_ref().map(|a| a.total_count).unwrap_or(0);
if anim_count > 10 {
critical.push(PerfCriticalQualifier::NonCompositedAnimations(anim_count));
}
let mut positive = Vec::new();
if p.vitals.cls.as_ref().is_some_and(|v| v.rating == "good") {
positive.push(PerfPositiveQualifier::LayoutStability);
}
if p.vitals.tbt.as_ref().is_some_and(|v| v.rating == "good") {
positive.push(PerfPositiveQualifier::MainThreadBlocking);
}
(critical, positive)
}
fn critical_qualifier_text(q: PerfCriticalQualifier, en: bool) -> String {
match q {
PerfCriticalQualifier::HighDomComplexity(nodes) => {
if en {
format!("high DOM complexity ({nodes} nodes)")
} else {
format!("die hohe DOM-Komplexität ({nodes} Knoten)")
}
}
PerfCriticalQualifier::LateThrottledLcp(ms) => {
if en {
format!("a late largest contentful paint under throttling ({ms:.0} ms)")
} else {
format!("der spät erscheinende Hauptinhalt unter Drosselung ({ms:.0} ms LCP)")
}
}
PerfCriticalQualifier::RenderBlocking(n) => {
if en {
format!("{n} render-blocking resources")
} else {
format!("{n} render-blockierende Ressourcen")
}
}
PerfCriticalQualifier::UnminifiedAssets(kb) => {
if en {
format!("unminified assets (~{kb:.0} KB savings)")
} else {
format!("unminifizierte Assets (~{kb:.0} KB Einsparpotenzial)")
}
}
PerfCriticalQualifier::NonCompositedAnimations(n) => {
if en {
format!("{n} non-composited animations")
} else {
format!("{n} nicht-composited Animationen")
}
}
}
}
fn positive_qualifier_text(q: PerfPositiveQualifier, en: bool) -> &'static str {
match q {
PerfPositiveQualifier::LayoutStability => {
if en {
"layout stability (CLS)"
} else {
"Layout-Stabilität (CLS)"
}
}
PerfPositiveQualifier::MainThreadBlocking => {
if en {
"main-thread blocking (TBT)"
} else {
"Hauptthread-Blockierung (TBT)"
}
}
}
}
fn join_phrases(items: &[String], en: bool) -> String {
match items.len() {
0 => String::new(),
1 => items[0].clone(),
_ => {
let (last, head) = items.split_last().unwrap();
format!(
"{} {} {}",
head.join(", "),
if en { "and" } else { "und" },
last
)
}
}
}
pub fn append_performance_qualifiers_text(
base: String,
p: &PerformanceResults,
throttled_lcp_max: f64,
en: bool,
) -> String {
let (critical, positive) = derive_performance_qualifiers(p, throttled_lcp_max);
if critical.is_empty() {
return base;
}
let critical_phrases: Vec<String> = critical
.iter()
.map(|q| critical_qualifier_text(*q, en))
.collect();
let positive_phrases: Vec<String> = positive
.iter()
.map(|q| positive_qualifier_text(*q, en).to_string())
.collect();
let mut out = base;
if !out.is_empty() {
out.push(' ');
}
if en {
out.push_str(&format!(
"Critical here are {}.",
join_phrases(&critical_phrases, en)
));
if !positive_phrases.is_empty() {
let verb = if positive_phrases.len() == 1 {
"remains"
} else {
"remain"
};
out.push_str(&format!(
" In contrast, {} {verb} unobtrusive.",
join_phrases(&positive_phrases, en)
));
}
} else {
out.push_str(&format!(
"Kritisch sind hier {}.",
join_phrases(&critical_phrases, en)
));
if !positive_phrases.is_empty() {
let verb = if positive_phrases.len() == 1 {
"bleibt"
} else {
"bleiben"
};
out.push_str(&format!(
" Im Kontrast dazu {verb} {} unauffällig.",
join_phrases(&positive_phrases, en)
));
}
}
out
}
pub fn performance_gap_text(
base: String,
p: &PerformanceResults,
cwv_all_good: bool,
score_below_excellent: bool,
has_render_blocking: bool,
en: bool,
) -> String {
if !(cwv_all_good && score_below_excellent) {
return base;
}
let mut reasons = Vec::new();
if p.vitals.dom_nodes.is_some_and(|n| n > 1500) {
reasons.push(if en { "DOM size" } else { "DOM-Größe" });
}
if has_render_blocking {
reasons.push(if en {
"render-blocking resources"
} else {
"Render-blockierende Ressourcen"
});
}
if p.vitals.tbt.as_ref().is_some_and(|v| v.rating != "good") {
reasons.push("Total Blocking Time");
}
if reasons.is_empty() {
base
} else if en {
format!(
"{} Score reduced by {} although Core Web Vitals are in the green.",
base,
reasons.join(", ")
)
} else {
format!(
"{} Score durch {} reduziert, obwohl Core Web Vitals im grünen Bereich liegen.",
base,
reasons.join(", ")
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn critical_qualifier_text_is_localized_and_en_has_no_german_umlauts() {
let cases = [
PerfCriticalQualifier::HighDomComplexity(4200),
PerfCriticalQualifier::LateThrottledLcp(5200.0),
PerfCriticalQualifier::RenderBlocking(7),
PerfCriticalQualifier::UnminifiedAssets(180.0),
PerfCriticalQualifier::NonCompositedAnimations(14),
];
for q in cases {
let en = critical_qualifier_text(q, true);
assert!(
!en.chars().any(|c| "äöüÄÖÜß".contains(c)),
"EN qualifier contains umlaut: {en}"
);
}
assert!(
critical_qualifier_text(PerfCriticalQualifier::HighDomComplexity(4200), true)
.contains("4200")
);
assert!(
critical_qualifier_text(PerfCriticalQualifier::HighDomComplexity(4200), false)
.contains("Komplexität")
);
}
#[test]
fn join_phrases_uses_locale_conjunction() {
let items = vec!["a".to_string(), "b".to_string(), "c".to_string()];
assert_eq!(join_phrases(&items, true), "a, b and c");
assert_eq!(join_phrases(&items, false), "a, b und c");
assert_eq!(join_phrases(&["solo".to_string()], true), "solo");
}
}