use std::collections::BTreeMap;
#[must_use]
pub fn negotiate(tag: &str, supported: &[&str], default: &str) -> String {
let lang = tag.split(['-', '_']).next().unwrap_or("").to_ascii_lowercase();
supported
.iter()
.find(|s| s.eq_ignore_ascii_case(&lang))
.map_or_else(|| default.to_string(), |s| (*s).to_string())
}
#[derive(Clone, Debug, Default)]
pub struct Catalog {
default: String,
entries: BTreeMap<&'static str, BTreeMap<&'static str, &'static str>>,
}
impl Catalog {
#[must_use]
pub fn new(default_lang: &str) -> Self {
Self { default: default_lang.to_string(), entries: BTreeMap::new() }
}
#[must_use]
pub fn with(mut self, key: &'static str, langs: &[(&'static str, &'static str)]) -> Self {
self.entries.entry(key).or_default().extend(langs.iter().copied());
self
}
#[must_use]
pub fn tr<'a>(&'a self, key: &'a str, lang: &str) -> &'a str {
let Some(by_lang) = self.entries.get(key) else { return key };
by_lang.get(lang).or_else(|| by_lang.get(self.default.as_str())).copied().unwrap_or(key)
}
}
#[cfg(test)]
mod tests {
use super::*;
const SUPPORTED: [&str; 5] = ["en", "de", "fr", "it", "uk"];
#[test]
fn negotiate_matches_language_subtag() {
assert_eq!(negotiate("de-CH", &SUPPORTED, "en"), "de");
assert_eq!(negotiate("uk_UA", &SUPPORTED, "en"), "uk"); assert_eq!(negotiate("FR", &SUPPORTED, "en"), "fr"); assert_eq!(negotiate("pt-BR", &SUPPORTED, "en"), "en"); assert_eq!(negotiate("", &SUPPORTED, "en"), "en");
}
#[test]
fn tr_falls_back_language_then_key() {
let cat = Catalog::new("en")
.with("ok", &[("en", "OK"), ("de", "OK"), ("uk", "Гаразд")])
.with("save", &[("en", "Save"), ("uk", "Зберегти")]);
assert_eq!(cat.tr("save", "uk"), "Зберегти"); assert_eq!(cat.tr("save", "de"), "Save"); assert_eq!(cat.tr("save", ""), "Save"); assert_eq!(cat.tr("missing", "uk"), "missing"); assert_eq!(cat.tr("ok", "uk"), "Гаразд");
}
#[test]
fn with_extends_an_existing_key() {
let cat = Catalog::new("en").with("x", &[("en", "X")]).with("x", &[("de", "X-de")]);
assert_eq!(cat.tr("x", "en"), "X");
assert_eq!(cat.tr("x", "de"), "X-de");
}
}