#![allow(clippy::missing_panics_doc)]
use http::Request;
use http::header::COOKIE;
use proptest::prelude::*;
use super::*;
#[test]
fn code_and_from_code_round_trip() {
for lang in Lang::all() {
assert_eq!(Lang::from_code(lang.code()), Some(lang));
}
}
#[test]
fn from_code_is_case_insensitive_and_takes_primary_subtag() {
assert_eq!(Lang::from_code("DE"), Some(Lang::De));
assert_eq!(Lang::from_code("de-DE"), Some(Lang::De));
assert_eq!(Lang::from_code("fr_CA"), Some(Lang::Fr));
assert_eq!(Lang::from_code("uk-UA"), Some(Lang::Uk));
assert_eq!(Lang::from_code("zz"), None);
assert_eq!(Lang::from_code(""), None);
}
#[test]
fn iso_639_3_primary_subtags_parse() {
assert_eq!(Lang::from_code("cnr"), Some(Lang::Cnr));
assert_eq!(Lang::from_code("smj"), Some(Lang::Smj));
assert_eq!(Lang::from_code("sma-NO"), Some(Lang::Sma));
}
#[test]
fn the_nb_alias_maps_bokmal_browsers_to_norwegian() {
assert_eq!(Lang::from_code("nb"), Some(Lang::No));
assert_eq!(Lang::from_code("nb-NO"), Some(Lang::No));
assert_eq!(Lang::No.code(), "no");
assert_eq!(Lang::from_code("nn"), Some(Lang::Nn));
}
#[test]
fn menu_has_49_languages_with_the_first_five_pinned() {
let all = Lang::all();
assert_eq!(all.len(), 49);
assert_eq!(
&all[..5],
&[Lang::En, Lang::De, Lang::Fr, Lang::Es, Lang::It]
);
assert_eq!(all[5], Lang::Sq, "tail starts at Albanian");
assert_eq!(all[48], Lang::Ur, "tail ends at Urdu");
}
#[test]
fn asian_additions_are_placed_by_english_name() {
let codes: Vec<&str> = Lang::all().iter().map(|l| l.code()).collect();
let zh = codes.iter().position(|&c| c == "zh").expect("zh present");
assert_eq!(codes[zh - 1], "ca", "Chinese follows Catalan");
assert_eq!(codes[zh + 1], "hr", "Chinese precedes Croatian");
let hi = codes.iter().position(|&c| c == "hi").expect("hi present");
assert_eq!(codes[hi - 1], "el", "Hindi follows Greek");
assert_eq!(codes[hi + 1], "hu", "Hindi precedes Hungarian");
}
#[test]
fn urdu_is_rtl_and_every_other_language_is_ltr() {
assert_eq!(Lang::Ur.dir(), "rtl");
for lang in Lang::all() {
let expected = if lang == Lang::Ur { "rtl" } else { "ltr" };
assert_eq!(lang.dir(), expected, "{} direction", lang.code());
}
}
#[test]
fn asian_subtags_parse_without_an_alias_table() {
assert_eq!(Lang::from_code("zh"), Some(Lang::Zh));
assert_eq!(Lang::from_code("hi"), Some(Lang::Hi));
assert_eq!(Lang::from_code("ur"), Some(Lang::Ur));
assert_eq!(Lang::from_code("zh-CN"), Some(Lang::Zh));
assert_eq!(Lang::from_code("zh-Hans"), Some(Lang::Zh));
assert_eq!(Lang::from_code("zh-Hant-TW"), Some(Lang::Zh));
assert_eq!(Lang::from_code("ur-PK"), Some(Lang::Ur));
}
#[test]
fn codes_and_endonyms_are_all_distinct_and_nonempty() {
let mut codes = std::collections::HashSet::new();
let mut endonyms = std::collections::HashSet::new();
for lang in Lang::all() {
assert!(!lang.code().is_empty());
assert!(!lang.endonym().is_empty());
assert!(codes.insert(lang.code()), "duplicate code {}", lang.code());
assert!(
endonyms.insert(lang.endonym()),
"duplicate endonym {}",
lang.endonym()
);
}
}
#[test]
fn every_language_has_an_embedded_table() {
for lang in Lang::all() {
assert!(
tables().contains_key(lang.code()),
"no embedded TSV for {}",
lang.code()
);
}
}
#[test]
fn every_english_key_resolves_nonempty_in_every_language() {
let en = tables().get("en").expect("English table exists");
assert!(!en.is_empty(), "English table must not be empty");
for key in en.keys() {
for lang in Lang::all() {
let text = t(lang, key);
assert!(!text.is_empty(), "{key} resolves empty in {}", lang.code());
assert_ne!(
text,
*key,
"{key} fell through to the raw key in {}",
lang.code()
);
}
}
}
#[test]
fn no_translation_carries_a_key_english_lacks() {
let en = tables().get("en").expect("English table exists");
for lang in Lang::all() {
let table = tables().get(lang.code()).expect("table exists");
for key in table.keys() {
assert!(
en.contains_key(key),
"{} carries orphan key {key}",
lang.code()
);
}
}
}
#[test]
fn missing_keys_fall_back_to_the_english_text() {
let en = tables().get("en").expect("English table exists");
for lang in Lang::all() {
let table = tables().get(lang.code()).expect("table exists");
for (key, en_text) in en {
if !table.contains_key(key) {
assert_eq!(
t(lang, key),
*en_text,
"missing {key} in {} must yield the English TEXT",
lang.code()
);
}
}
}
}
#[test]
fn unknown_key_returns_the_key_itself() {
assert_eq!(t(Lang::De, "no.such.key"), "no.such.key");
}
#[test]
fn placeholders_survive_in_every_translation() {
let en = tables().get("en").expect("English table exists");
for (key, en_text) in en {
let mut placeholders: Vec<&str> = Vec::new();
let mut rest = *en_text;
while let Some(start) = rest.find('{') {
if let Some(len) = rest[start..].find('}') {
placeholders.push(&rest[start..=start + len]);
rest = &rest[start + len + 1..];
} else {
break;
}
}
if placeholders.is_empty() {
continue;
}
for lang in Lang::all() {
let text = t(lang, key);
for ph in &placeholders {
assert!(
text.contains(ph),
"{key} in {} lost placeholder {ph}",
lang.code()
);
}
}
}
}
#[test]
fn protocol_fixed_vocabulary_is_identical_in_every_language() {
const FIXED: &[&str] = &[
"admin.format_csv_label",
"admin.format_json_label",
"admin.format_markdown_label",
"admin.format_sarif_label",
"csaf.field_cve",
"csaf.field_cwe_id",
"csaf.field_nato",
"csaf.field_tlp",
"csaf.field_verschlusssache",
"csaf.form_section_cvss_v3",
"csaf.form_section_cvss_v4",
"csaf.list_col_cvss_v3",
"csaf.list_col_cvss_v4",
"csaf.opt_category_vex",
"csaf.view_json_button",
"csaf.view_row_tlp",
"csaf.view_vuln_col_cve",
"info.license_spdx_label",
"nav.csaf",
"nav.dashboard",
"nav.info",
];
for key in FIXED {
let english = t(Lang::En, key);
assert_ne!(english, *key, "{key} must exist in the English table");
for lang in Lang::all() {
assert_eq!(
t(lang, key),
english,
"{key} must stay protocol-fixed (untranslated) in {}",
lang.code()
);
}
}
}
#[test]
fn parse_tsv_skips_comments_and_blanks_and_splits_on_first_tab() {
let table = parse_tsv("# header\twith a tab\n\nnav.a\tHello\tworld\nnav.b\tX\n");
assert_eq!(table.len(), 2);
assert_eq!(table.get("nav.a"), Some(&"Hello\tworld"));
assert_eq!(table.get("nav.b"), Some(&"X"));
}
#[test]
fn parse_tsv_drops_lines_without_a_tab() {
let table = parse_tsv("no-tab-here\nnav.a\tok\n");
assert_eq!(table.len(), 1);
assert_eq!(table.get("nav.a"), Some(&"ok"));
}
#[test]
fn cookie_beats_accept_language() {
assert_eq!(resolve(Some("csaf_lang=de"), Some("fr")), Lang::De);
}
#[test]
fn accept_language_used_when_no_cookie() {
assert_eq!(resolve(None, Some("es,en;q=0.5")), Lang::Es);
assert_eq!(resolve(None, Some("nb-NO,en;q=0.7")), Lang::No);
}
#[test]
fn accept_language_excludes_q_zero_languages() {
assert_eq!(resolve(None, Some("de;q=0, fr")), Lang::Fr);
}
#[test]
fn unresolvable_headers_fall_back_to_english() {
assert_eq!(resolve(None, None), Lang::En);
assert_eq!(resolve(Some("other=1"), Some("xx-XX")), Lang::En);
}
#[test]
fn cookie_parsing_finds_the_pair_among_others() {
assert_eq!(resolve(Some("a=1; csaf_lang=it; b=2"), None), Lang::It);
}
#[test]
fn malformed_cookie_pairs_do_not_panic_or_match() {
assert_eq!(resolve(Some("csaf_lang; =de; ==="), None), Lang::En);
}
#[test]
fn from_parts_reads_every_cookie_header_field() {
let (parts, ()) = Request::builder()
.header(COOKIE, "unrelated_app=1")
.header(COOKIE, "csaf_lang=de")
.body(())
.expect("request builds")
.into_parts();
assert_eq!(from_parts(&parts), Lang::De);
}
proptest! {
#[test]
fn resolve_is_total_over_arbitrary_headers(
cookie in prop::option::of(".*"),
accept in prop::option::of(".*"),
) {
prop_assert!(Lang::all().contains(&resolve(cookie.as_deref(), accept.as_deref())));
}
#[test]
fn from_code_round_trips_with_the_nb_alias_exception(junk in ".*") {
if let Some(parsed) = Lang::from_code(&junk) {
let primary = junk
.split(['-', '_'])
.next()
.unwrap_or(&junk)
.to_ascii_lowercase();
let aliased = primary == "nb" && parsed.code() == "no";
prop_assert!(primary == parsed.code() || aliased);
}
}
#[test]
fn the_cookie_pin_wins(idx in 0usize..49) {
let lang = Lang::all()[idx];
let cookie = format!("csaf_lang={}", lang.code());
prop_assert_eq!(resolve(Some(&cookie), Some("zz,qq")), lang);
}
#[test]
fn t_is_total(idx in 0usize..49, key in "[a-z._]{0,40}") {
let lang = Lang::all()[idx];
let leaked: &'static str = Box::leak(key.into_boxed_str());
let text = t(lang, leaked);
prop_assert!(!text.is_empty() || leaked.is_empty());
}
}