use std::collections::HashMap;
use std::sync::OnceLock;
use http::header::{ACCEPT_LANGUAGE, COOKIE};
use http::request::Parts;
mod lang;
#[cfg(test)]
mod tests;
pub use lang::Lang;
pub const COOKIE_NAME: &str = "csaf_lang";
type Table = HashMap<&'static str, &'static str>;
static TSV: [(&str, &str); 49] = [
("en", include_str!("../i18n_data/en.tsv")),
("de", include_str!("../i18n_data/de.tsv")),
("fr", include_str!("../i18n_data/fr.tsv")),
("es", include_str!("../i18n_data/es.tsv")),
("it", include_str!("../i18n_data/it.tsv")),
("sq", include_str!("../i18n_data/sq.tsv")),
("hy", include_str!("../i18n_data/hy.tsv")),
("az", include_str!("../i18n_data/az.tsv")),
("be", include_str!("../i18n_data/be.tsv")),
("bs", include_str!("../i18n_data/bs.tsv")),
("bg", include_str!("../i18n_data/bg.tsv")),
("ca", include_str!("../i18n_data/ca.tsv")),
("zh", include_str!("../i18n_data/zh.tsv")),
("hr", include_str!("../i18n_data/hr.tsv")),
("cs", include_str!("../i18n_data/cs.tsv")),
("da", include_str!("../i18n_data/da.tsv")),
("nl", include_str!("../i18n_data/nl.tsv")),
("et", include_str!("../i18n_data/et.tsv")),
("fi", include_str!("../i18n_data/fi.tsv")),
("ka", include_str!("../i18n_data/ka.tsv")),
("el", include_str!("../i18n_data/el.tsv")),
("hi", include_str!("../i18n_data/hi.tsv")),
("hu", include_str!("../i18n_data/hu.tsv")),
("is", include_str!("../i18n_data/is.tsv")),
("ga", include_str!("../i18n_data/ga.tsv")),
("la", include_str!("../i18n_data/la.tsv")),
("lv", include_str!("../i18n_data/lv.tsv")),
("lt", include_str!("../i18n_data/lt.tsv")),
("smj", include_str!("../i18n_data/smj.tsv")),
("lb", include_str!("../i18n_data/lb.tsv")),
("mk", include_str!("../i18n_data/mk.tsv")),
("mt", include_str!("../i18n_data/mt.tsv")),
("cnr", include_str!("../i18n_data/cnr.tsv")),
("se", include_str!("../i18n_data/se.tsv")),
("no", include_str!("../i18n_data/no.tsv")),
("nn", include_str!("../i18n_data/nn.tsv")),
("pl", include_str!("../i18n_data/pl.tsv")),
("pt", include_str!("../i18n_data/pt.tsv")),
("ro", include_str!("../i18n_data/ro.tsv")),
("rm", include_str!("../i18n_data/rm.tsv")),
("ru", include_str!("../i18n_data/ru.tsv")),
("sr", include_str!("../i18n_data/sr.tsv")),
("sk", include_str!("../i18n_data/sk.tsv")),
("sl", include_str!("../i18n_data/sl.tsv")),
("sma", include_str!("../i18n_data/sma.tsv")),
("sv", include_str!("../i18n_data/sv.tsv")),
("tr", include_str!("../i18n_data/tr.tsv")),
("uk", include_str!("../i18n_data/uk.tsv")),
("ur", include_str!("../i18n_data/ur.tsv")),
];
fn tables() -> &'static HashMap<&'static str, Table> {
static TABLES: OnceLock<HashMap<&'static str, Table>> = OnceLock::new();
TABLES.get_or_init(|| {
TSV.iter()
.map(|&(code, src)| (code, parse_tsv(src)))
.collect()
})
}
fn parse_tsv(src: &'static str) -> Table {
src.lines()
.filter(|line| !line.is_empty() && !line.starts_with('#'))
.filter_map(|line| line.split_once('\t'))
.collect()
}
#[must_use]
pub fn t(lang: Lang, key: &'static str) -> &'static str {
let tables = tables();
if let Some(text) = tables.get(lang.code()).and_then(|t| t.get(key).copied()) {
return text;
}
if let Some(text) = tables.get("en").and_then(|t| t.get(key).copied()) {
return text;
}
key
}
#[must_use]
pub fn translation_keys() -> Vec<&'static str> {
tables()
.get("en")
.map(|table| table.keys().copied().collect())
.unwrap_or_default()
}
#[must_use]
pub fn from_parts(parts: &Parts) -> Lang {
let cookie = parts
.headers
.get_all(COOKIE)
.iter()
.filter_map(|value| value.to_str().ok())
.collect::<Vec<_>>()
.join("; ");
let cookie = (!cookie.is_empty()).then_some(cookie.as_str());
let accept = parts
.headers
.get(ACCEPT_LANGUAGE)
.and_then(|v| v.to_str().ok());
resolve(cookie, accept)
}
#[must_use]
pub fn resolve(cookie_header: Option<&str>, accept_language: Option<&str>) -> Lang {
if let Some(lang) = cookie_header.and_then(lang_from_cookie) {
return lang;
}
if let Some(lang) = accept_language.and_then(lang_from_accept) {
return lang;
}
Lang::En
}
fn lang_from_cookie(header: &str) -> Option<Lang> {
header.split(';').find_map(|pair| {
let (name, value) = pair.split_once('=')?;
(name.trim() == COOKIE_NAME).then(|| Lang::from_code(value.trim()))?
})
}
fn lang_from_accept(header: &str) -> Option<Lang> {
header.split(',').find_map(|item| {
let mut params = item.split(';');
let tag = params.next().unwrap_or(item).trim();
let excluded = params.any(|param| {
param
.trim()
.strip_prefix("q=")
.is_some_and(|q| q.trim().parse::<f32>() == Ok(0.0))
});
if excluded {
return None;
}
Lang::from_code(tag)
})
}