use crate::i18n::I18n;
use crate::output::localized::is_english;
use crate::output::report_model::*;
pub(super) fn manual_recheck_instruction(wcag_criterion: &str, en: bool) -> Option<&'static str> {
match wcag_criterion {
"1.3.6" => Some(if en {
"Self-check: open VoiceOver's Rotor (VO+U, macOS) or NVDA's Elements List \
(Insert+F7) and switch to \"Landmarks\" — listen whether the areas you'd expect \
(header, navigation, footer) are actually announced."
} else {
"Selbsttest: VoiceOver-Rotor (VO+U, macOS) oder NVDA-Elementliste (Einfg+F7) öffnen \
und auf „Landmarken\" umschalten — anhören, ob die erwarteten Bereiche (Header, \
Navigation, Footer) tatsächlich angekündigt werden."
}),
"1.3.1" => Some(if en {
"Self-check: use VoiceOver's Rotor (VO+U) or NVDA's Elements List (Insert+F7) and \
check both the \"Headings\" and \"Landmarks\" views — confirm the order/uniqueness \
you hear matches what this finding describes."
} else {
"Selbsttest: VoiceOver-Rotor (VO+U) oder NVDA-Elementliste (Einfg+F7) nutzen und \
sowohl „Überschriften\" als auch „Landmarken\" prüfen — abgleichen, ob \
Reihenfolge/Eindeutigkeit dem hier beschriebenen Befund entspricht."
}),
"3.3.2" => Some(if en {
"Self-check: tab into the form with VoiceOver (VO+Cmd+J jumps between form \
controls) or NVDA (F jumps between fields) and listen whether every field \
announces a name — not just a generic \"edit text\"."
} else {
"Selbsttest: mit VoiceOver (VO+Cmd+J springt zwischen Formularfeldern) oder NVDA \
(F springt zwischen Feldern) durch das Formular tabben und hören, ob jedes Feld \
einen Namen ansagt — nicht nur ein generisches „Textfeld\"."
}),
"4.1.3" => Some(if en {
"Self-check: trigger the message (e.g. submit the form with an invalid value) \
while VoiceOver or NVDA is running and confirm it is announced automatically, \
without moving keyboard focus to it."
} else {
"Selbsttest: die Meldung auslösen (z. B. Formular mit ungültigem Wert absenden), \
während VoiceOver oder NVDA läuft, und prüfen, ob sie automatisch angesagt wird, \
ohne dass der Tastaturfokus dorthin springt."
}),
_ => None,
}
}
const SECTION_HEADER_SPLIT_TEMPLATE: &str = include_str!("templates/section_header_split.typ");
const METRIC_STRIP_TEMPLATE: &str = include_str!("templates/metric_strip.typ");
const ARIMO_REGULAR: &[u8] = include_bytes!("../../../assets/fonts/arimo/Arimo-Regular.ttf");
const ARIMO_BOLD: &[u8] = include_bytes!("../../../assets/fonts/arimo/Arimo-Bold.ttf");
const ARIMO_ITALIC: &[u8] = include_bytes!("../../../assets/fonts/arimo/Arimo-Italic.ttf");
const ARIMO_BOLD_ITALIC: &[u8] = include_bytes!("../../../assets/fonts/arimo/Arimo-BoldItalic.ttf");
const PT_SERIF_REGULAR: &[u8] =
include_bytes!("../../../assets/fonts/ptserif/PT_Serif-Web-Regular.ttf");
const PT_SERIF_BOLD: &[u8] = include_bytes!("../../../assets/fonts/ptserif/PT_Serif-Web-Bold.ttf");
const PT_SERIF_ITALIC: &[u8] =
include_bytes!("../../../assets/fonts/ptserif/PT_Serif-Web-Italic.ttf");
const PT_SERIF_BOLD_ITALIC: &[u8] =
include_bytes!("../../../assets/fonts/ptserif/PT_Serif-Web-BoldItalic.ttf");
const JETBRAINS_MONO_REGULAR: &[u8] =
include_bytes!("../../../assets/fonts/jetbrainsmono/JetBrainsMono-Regular.ttf");
const JETBRAINS_MONO_BOLD: &[u8] =
include_bytes!("../../../assets/fonts/jetbrainsmono/JetBrainsMono-Bold.ttf");
const THEME_FONTS: &[&[u8]] = &[
ARIMO_REGULAR,
ARIMO_BOLD,
ARIMO_ITALIC,
ARIMO_BOLD_ITALIC,
PT_SERIF_REGULAR,
PT_SERIF_BOLD,
PT_SERIF_ITALIC,
PT_SERIF_BOLD_ITALIC,
JETBRAINS_MONO_REGULAR,
JETBRAINS_MONO_BOLD,
];
static SHARED_ENGINE: std::sync::LazyLock<std::sync::Arc<renderreport::Engine>> =
std::sync::LazyLock::new(|| {
std::sync::Arc::new(build_engine().expect("PDF engine should build"))
});
pub(super) fn create_engine() -> std::sync::Arc<renderreport::Engine> {
std::sync::Arc::clone(&SHARED_ENGINE)
}
fn build_engine() -> anyhow::Result<renderreport::Engine> {
use renderreport::components::ComponentId;
use renderreport::engine::EngineConfig;
use renderreport::theme::{Theme, TokenValue};
let config = EngineConfig::builder().use_system_fonts(false).build();
let mut engine = renderreport::Engine::with_config_and_fonts(config, THEME_FONTS)?;
engine.components_mut().register(
ComponentId::new("section-header-split"),
SECTION_HEADER_SPLIT_TEMPLATE.to_string(),
);
engine.components_mut().register(
ComponentId::new("metric-strip"),
METRIC_STRIP_TEMPLATE.to_string(),
);
let mut theme = Theme::default_theme();
theme
.tokens
.set("font.body", TokenValue::Font("Arimo".into()));
theme
.tokens
.set("font.heading", TokenValue::Font("PT Serif".into()));
theme
.tokens
.set("font.mono", TokenValue::Font("JetBrains Mono".into()));
engine.set_default_theme(theme);
Ok(engine)
}
pub(super) fn map_severity(severity: &crate::wcag::Severity) -> renderreport::prelude::Severity {
use renderreport::prelude::Severity;
match severity {
crate::wcag::Severity::Critical => Severity::Critical,
crate::wcag::Severity::High => Severity::High,
crate::wcag::Severity::Medium => Severity::Medium,
crate::wcag::Severity::Low => Severity::Low,
}
}
pub(super) fn severity_label_i18n(severity: crate::wcag::Severity, i18n: &I18n) -> String {
match severity {
crate::wcag::Severity::Critical => i18n.t("severity-critical"),
crate::wcag::Severity::High => i18n.t("severity-high"),
crate::wcag::Severity::Medium => i18n.t("severity-medium"),
crate::wcag::Severity::Low => i18n.t("severity-low"),
}
}
pub(super) fn priority_label_i18n(priority: Priority, i18n: &I18n) -> String {
match priority {
Priority::Critical => i18n.t("priority-critical"),
Priority::High => i18n.t("priority-high"),
Priority::Medium => i18n.t("priority-medium"),
Priority::Low => i18n.t("priority-low"),
}
}
pub(super) fn role_label_i18n(role: Role, i18n: &I18n) -> String {
match role {
Role::Development => i18n.t("role-development"),
Role::Editorial => i18n.t("role-editorial"),
Role::DesignUx => i18n.t("role-designux"),
Role::ProjectManagement => i18n.t("role-projectmanagement"),
}
}
pub(super) fn effort_label_i18n(effort: Effort, i18n: &I18n) -> String {
match effort {
Effort::Quick => i18n.t("effort-quick"),
Effort::Medium => i18n.t("effort-medium"),
Effort::Structural => i18n.t("effort-structural"),
}
}
pub(super) fn score_quality_label(score: u32) -> &'static str {
match score {
85..=100 => "Stark",
70..=84 => "Solide",
50..=69 => "Uneinheitlich",
_ => "Schwach",
}
}
pub(super) fn score_quality_color(score: u32) -> &'static str {
use super::design::tokens;
match score {
70..=100 => tokens::SUCCESS,
50..=69 => tokens::WARN_DEEP,
_ => tokens::DANGER,
}
}
pub(super) fn truncate_with_ellipsis(value: &str, max_chars: usize) -> String {
let count = value.chars().count();
if count <= max_chars {
return value.to_string();
}
value
.chars()
.take(max_chars.saturating_sub(1))
.collect::<String>()
+ "…"
}
pub(super) fn module_name_with_taxonomy_suffix(
name: &str,
measurement_type: &str,
i18n: &I18n,
) -> String {
use crate::output::report_model::ModuleTaxonomyClass;
let class = ModuleTaxonomyClass::from_measurement_type(measurement_type, name);
if !class.needs_suffix_qualifier() {
return name.to_string();
}
let en = is_english(i18n);
let suffix = match class {
ModuleTaxonomyClass::Optional => "Optional",
_ => {
if en {
"Indicator"
} else {
"Indikator"
}
}
};
format!("{name} ({suffix})")
}
#[cfg(test)]
mod tests {
use super::{METRIC_STRIP_TEMPLATE, SECTION_HEADER_SPLIT_TEMPLATE};
#[test]
fn section_eyebrow_uses_light_spacious_typography() {
assert!(SECTION_HEADER_SPLIT_TEMPLATE.contains("weight: \"regular\""));
assert!(SECTION_HEADER_SPLIT_TEMPLATE.contains("tracking: 0.20em"));
assert!(SECTION_HEADER_SPLIT_TEMPLATE.contains("#v(spacing-3)"));
}
#[test]
fn metric_value_and_context_share_a_bottom_alignment() {
assert!(METRIC_STRIP_TEMPLATE.contains("align: bottom + left"));
assert!(!METRIC_STRIP_TEMPLATE.contains("pad(top: 3pt)"));
}
#[test]
fn taxonomy_suffix_leaves_compliance_and_measured_names_untouched() {
let de = crate::i18n::I18n::new("de").expect("i18n");
assert_eq!(
super::module_name_with_taxonomy_suffix("Accessibility", "measured", &de),
"Accessibility"
);
assert_eq!(
super::module_name_with_taxonomy_suffix("Performance", "measured", &de),
"Performance"
);
assert_eq!(
super::module_name_with_taxonomy_suffix("Search Experience", "composite", &de),
"Search Experience"
);
}
#[test]
fn taxonomy_suffix_marks_heuristic_modules_as_indicator() {
let de = crate::i18n::I18n::new("de").expect("i18n");
let en = crate::i18n::I18n::new("en").expect("i18n");
assert_eq!(
super::module_name_with_taxonomy_suffix("UX", "heuristic", &de),
"UX (Indikator)"
);
assert_eq!(
super::module_name_with_taxonomy_suffix("UX", "heuristic", &en),
"UX (Indicator)"
);
}
#[test]
fn taxonomy_suffix_left_off_html_conformance_as_measured() {
let de = crate::i18n::I18n::new("de").expect("i18n");
assert_eq!(
super::module_name_with_taxonomy_suffix("HTML Conformance", "measured", &de),
"HTML Conformance"
);
}
#[test]
fn taxonomy_suffix_marks_optional_modules_as_optional() {
let de = crate::i18n::I18n::new("de").expect("i18n");
let en = crate::i18n::I18n::new("en").expect("i18n");
assert_eq!(
super::module_name_with_taxonomy_suffix("Dark Mode", "optional", &de),
"Dark Mode (Optional)"
);
assert_eq!(
super::module_name_with_taxonomy_suffix("Dark Mode", "optional", &en),
"Dark Mode (Optional)"
);
}
}