use std::collections::HashMap;
pub const FALLBACK_LANG: &str = "en";
#[derive(Debug, Clone, Default)]
pub struct I18n {
entries: HashMap<String, HashMap<String, String>>,
}
impl I18n {
pub fn new() -> Self {
Self::default()
}
pub fn add(mut self, key: &str, translations: &[(&str, &str)]) -> Self {
let map = self.entries.entry(key.to_owned()).or_default();
for (lang, text) in translations {
map.insert(lang.to_ascii_lowercase(), (*text).to_owned());
}
self
}
pub fn merge(mut self, other: I18n) -> Self {
for (key, langs) in other.entries {
let slot = self.entries.entry(key).or_default();
slot.extend(langs);
}
self
}
pub fn t(&self, lang: &str, key: &str) -> String {
let lang = lang.to_ascii_lowercase();
match self.entries.get(key) {
Some(langs) => langs
.get(&lang)
.or_else(|| langs.get(FALLBACK_LANG))
.cloned()
.unwrap_or_else(|| key.to_owned()),
None => key.to_owned(),
}
}
pub fn tf(&self, lang: &str, key: &str, args: &[&str]) -> String {
let template = self.t(lang, key);
let mut out = String::with_capacity(template.len());
let mut args = args.iter();
let mut chars = template.chars().peekable();
while let Some(c) = chars.next() {
if c == '{' && chars.peek() == Some(&'}') {
chars.next();
match args.next() {
Some(a) => out.push_str(a),
None => out.push_str("{}"),
}
} else {
out.push(c);
}
}
out
}
pub fn languages(&self) -> Vec<String> {
let mut set: Vec<String> = self
.entries
.values()
.flat_map(|langs| langs.keys().cloned())
.collect();
set.sort();
set.dedup();
set
}
pub fn missing(&self, langs: &[&str]) -> Vec<(String, String)> {
let mut gaps = Vec::new();
for (key, translations) in &self.entries {
for lang in langs {
let lc = lang.to_ascii_lowercase();
if !translations.contains_key(&lc) {
gaps.push((key.clone(), lc));
}
}
}
gaps.sort();
gaps
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cat() -> I18n {
I18n::new()
.add("hi", &[("en", "Hi"), ("ru", "Привет"), ("de", "Hallo")])
.add("bye", &[("en", "Bye {}"), ("ru", "Пока {}")])
}
#[test]
fn lookup_and_fallback() {
let c = cat();
assert_eq!(c.t("ru", "hi"), "Привет");
assert_eq!(c.t("de", "hi"), "Hallo");
assert_eq!(c.t("fr", "hi"), "Hi"); assert_eq!(c.t("en", "missing"), "missing"); }
#[test]
fn case_insensitive_lang() {
assert_eq!(cat().t("RU", "hi"), "Привет");
}
#[test]
fn formatting_fills_placeholders() {
let c = cat();
assert_eq!(c.tf("en", "bye", &["Sam"]), "Bye Sam");
assert_eq!(c.tf("ru", "bye", &["Сэм"]), "Пока Сэм");
assert_eq!(c.tf("en", "bye", &[]), "Bye {}");
}
#[test]
fn languages_are_unique_and_sorted() {
assert_eq!(cat().languages(), vec!["de", "en", "ru"]);
}
#[test]
fn missing_reports_gaps() {
let c = cat();
let gaps = c.missing(&["en", "ru", "de"]);
assert_eq!(gaps, vec![("bye".to_owned(), "de".to_owned())]);
assert!(c.missing(&["en"]).is_empty());
}
}