csaf-crud 1.4.15

CSAF 2.0 / 2.1 advisory CRUD server with HATEOAS JSON API and HTML UI (TLS 1.3, HTTP/1.1 + HTTP/2 + HTTP/3)
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2026 Pierre Gronau, ndaal in Cologne

//! CSAF document CRUD (`routes::csaf`) page translations.
//!
//! Covers the list, create/edit form, view/detail, and error-page chrome
//! rendered by `routes/csaf.rs`. A flat single-file match table for this
//! domain would run well past 800 lines (100+ keys × 5 languages), so —
//! same as the reason `i18n` itself is split one-file-per-route-module
//! instead of one 1831-line file — this domain is a directory module,
//! split one file per page/concern (`list`, `form_chrome`, `form_fields`,
//! `options`, `view`, `errors`), each exposing its own `pub(super) fn tr`
//! chained via `.or_else()` in this file's [`tr`]. `i18n/mod.rs` is
//! unaffected: `mod csaf;` resolves to this `mod.rs` exactly as it
//! resolved to the old flat `csaf.rs`, and `csaf::tr` is still the only
//! item it calls.
//!
//! What is intentionally NOT in this table (left as literal English, or
//! untouched): CSAF document content and data values (tracking IDs, CVE
//! IDs, CVSS vectors, note/title sentinels written into exported JSON,
//! `placeholder="..."` example values); every protocol-fixed whitelist
//! value (TLP labels, Verschlusssache/NATO labels, CSAF category
//! identifiers, CVSS severity words `NONE`/`LOW`/`MEDIUM`/`HIGH`/`CRITICAL`
//! — the last of these by analogy with TLP, since both are external,
//! cross-organisation standards always shown as literal uppercase English
//! tokens); and the `build_document_from_form` validation-error strings
//! (that function's signature is frozen by `fuzz-harness`, so it has no
//! way to receive a [`super::Lang`]).

use super::Entry;

mod errors;
mod form_chrome;
mod form_fields;
mod list;
mod options;
mod view;

/// Look up a `csaf.*` translation key across this domain's six
/// per-page/concern sub-modules.
pub(super) fn tr(key: &str) -> Option<Entry> {
    list::tr(key)
        .or_else(|| form_chrome::tr(key))
        .or_else(|| form_fields::tr(key))
        .or_else(|| options::tr(key))
        .or_else(|| view::tr(key))
        .or_else(|| errors::tr(key))
}

#[cfg(test)]
mod tests {
    use super::super::{Lang, t};
    use super::*;

    /// Every `csaf.*` key this module claims to translate. Kept as one flat
    /// list (mirroring `i18n::nav`'s own test) so a missing/typo'd key in
    /// any of the six sub-modules fails loudly here instead of silently
    /// falling back to the raw key at render time.
    const ALL_KEYS: &[&str] = &[
        // list
        "csaf.list_heading",
        "csaf.list_new_document",
        "csaf.list_empty",
        "csaf.list_create_one",
        "csaf.list_col_tracking_id",
        "csaf.list_col_title",
        "csaf.list_col_category",
        "csaf.list_col_status",
        "csaf.list_col_cvss_v3",
        "csaf.list_col_cvss_v4",
        "csaf.list_col_release_date",
        "csaf.list_pagination_previous",
        "csaf.list_pagination_next",
        "csaf.list_pagination_showing",
        "csaf.list_pagination_of",
        "csaf.list_documents_plural",
        // form chrome
        "csaf.form_new_title",
        "csaf.form_edit_title",
        "csaf.form_edit_heading_prefix",
        "csaf.form_submit_create",
        "csaf.form_submit_save",
        "csaf.form_cancel",
        "csaf.form_section_document_metadata",
        "csaf.form_section_publisher",
        "csaf.form_section_affected_product",
        "csaf.form_section_vulnerability",
        "csaf.form_vulnerability_hint",
        "csaf.form_section_cvss_v3",
        "csaf.form_section_cvss_v4",
        // form fields
        "csaf.field_tracking_id",
        "csaf.field_title",
        "csaf.field_category",
        "csaf.field_status",
        "csaf.field_doc_version",
        "csaf.field_tlp",
        "csaf.field_initial_release_date",
        "csaf.field_current_release_date",
        "csaf.field_summary",
        "csaf.field_publisher_name",
        "csaf.field_publisher_namespace",
        "csaf.field_publisher_contact",
        "csaf.field_vendor",
        "csaf.field_product_name",
        "csaf.field_product_id",
        "csaf.field_product_status",
        "csaf.field_cve",
        "csaf.field_vuln_title",
        "csaf.field_vuln_description",
        "csaf.field_cwe_id",
        "csaf.field_cwe_name",
        "csaf.field_cvss_base_score",
        "csaf.field_cvss_severity",
        "csaf.field_cvss_v3_vector",
        "csaf.field_cvss_v4_vector",
        "csaf.field_verschlusssache",
        "csaf.field_nato",
        // options
        "csaf.opt_category_security_advisory",
        "csaf.opt_category_vex",
        "csaf.opt_category_informational_advisory",
        "csaf.opt_status_draft",
        "csaf.opt_status_interim",
        "csaf.opt_status_final",
        "csaf.opt_product_status_known_affected",
        "csaf.opt_product_status_known_not_affected",
        "csaf.opt_product_status_fixed",
        "csaf.opt_product_status_under_investigation",
        "csaf.opt_publisher_category_vendor",
        "csaf.opt_publisher_category_discoverer",
        "csaf.opt_publisher_category_coordinator",
        "csaf.opt_publisher_category_user",
        "csaf.opt_publisher_category_translator",
        "csaf.opt_publisher_category_other",
        // view
        "csaf.view_edit_button",
        "csaf.view_json_button",
        "csaf.view_delete_button",
        "csaf.view_delete_confirm",
        "csaf.view_section_metadata",
        "csaf.view_section_vulnerabilities",
        "csaf.view_section_raw_json",
        "csaf.view_no_vulnerabilities",
        "csaf.view_row_csaf_version",
        "csaf.view_row_version",
        "csaf.view_row_tlp",
        "csaf.view_row_initial_release",
        "csaf.view_row_current_release",
        "csaf.view_vuln_col_cve",
        // errors
        "csaf.err_back",
        "csaf.err_title_create_failed",
        "csaf.err_title_not_found",
        "csaf.err_title_error",
        "csaf.err_title_duplicate",
        "csaf.err_title_storage_error",
        "csaf.err_title_update_failed",
        "csaf.err_title_tracking_id_mismatch",
        "csaf.err_title_validation_failed",
        "csaf.err_title_delete_error",
        "csaf.err_form_error_prefix",
        "csaf.err_document_exists",
        "csaf.err_document_not_found",
        "csaf.err_tracking_id_mismatch",
        "csaf.err_validation_prefix",
        "csaf.err_missing_document_id",
    ];

    /// Protocol-fixed whitelist values that must NEVER appear as a
    /// translation VALUE in this table (they are compared with `==` /
    /// whitelist functions elsewhere and must stay byte-identical in every
    /// language). Regression guard against accidentally "translating" one
    /// of them.
    const FORBIDDEN_VALUES: &[&str] = &[
        "CLEAR",
        "GREEN",
        "AMBER",
        "AMBER+STRICT",
        "RED",
        "VS-NfD (VS-NUR FÜR DEN DIENSTGEBRAUCH)",
        "VS-Vertr. (VS-VERTRAULICH)",
        "Geh. (GEHEIM)",
        "Str. Geh. (STRENG GEHEIM)",
        "NR (NATO RESTRICTED)",
        "NC (NATO CONFIDENTIAL)",
        "NS (NATO SECRET)",
        "CTS (COSMIC TOP SECRET)",
        "csaf_security_advisory",
        "csaf_vex",
        "csaf_informational_advisory",
        "sev-critical",
        "sev-high",
        "sev-medium",
        "sev-low",
        "sev-none",
        "NONE",
        "LOW",
        "MEDIUM",
        "HIGH",
        "CRITICAL",
    ];

    #[test]
    fn every_csaf_key_has_five_nonempty_translations() {
        for key in ALL_KEYS {
            let (en, de, fr, es, it) = tr(key).unwrap_or_else(|| panic!("{key} resolves"));
            assert!(
                !en.is_empty()
                    && !de.is_empty()
                    && !fr.is_empty()
                    && !es.is_empty()
                    && !it.is_empty(),
                "{key}"
            );
        }
    }

    #[test]
    fn unknown_key_is_not_claimed_by_this_table() {
        assert_eq!(tr("csaf.does_not_exist"), None);
    }

    #[test]
    fn t_dispatches_csaf_keys_through_the_shared_lookup() {
        assert_eq!(t(Lang::De, "csaf.list_heading"), "CSAF-Dokumente");
        assert_eq!(t(Lang::Fr, "csaf.form_cancel"), "Annuler");
        assert_eq!(t(Lang::It, "csaf.view_delete_button"), "Elimina");
    }

    #[test]
    fn no_translation_value_equals_a_protocol_fixed_whitelist_string() {
        for key in ALL_KEYS {
            let (en, de, fr, es, it) = tr(key).expect("key resolves");
            for (lang_name, value) in [("en", en), ("de", de), ("fr", fr), ("es", es), ("it", it)] {
                assert!(
                    !FORBIDDEN_VALUES.contains(&value),
                    "{key} ({lang_name}) = {value:?} matches a protocol-fixed whitelist value"
                );
            }
        }
    }
}