use std::collections::BTreeMap;
use std::sync::OnceLock;
use serde::Deserialize;
use crate::constants;
const CATALOGUES: &[&str] = &[
include_str!("locales/en.json"),
include_str!("locales/hi.json"),
include_str!("locales/te.json"),
include_str!("locales/ta.json"),
include_str!("locales/kn.json"),
include_str!("locales/ml.json"),
include_str!("locales/bn.json"),
include_str!("locales/mr.json"),
include_str!("locales/gu.json"),
include_str!("locales/pa.json"),
include_str!("locales/sa.json"),
include_str!("locales/zh.json"),
];
#[derive(Debug, Clone, Deserialize)]
pub struct Meta {
pub code: String,
pub english_name: String,
pub native_name: String,
pub reviewed: bool,
}
#[derive(Debug, Clone, Deserialize)]
struct Catalogue {
#[serde(rename = "_meta")]
meta: Meta,
#[serde(flatten)]
strings: BTreeMap<String, String>,
}
fn parsed() -> &'static Vec<Catalogue> {
static PARSED: OnceLock<Vec<Catalogue>> = OnceLock::new();
PARSED.get_or_init(|| {
CATALOGUES
.iter()
.filter_map(|raw| serde_json::from_str::<Catalogue>(raw).ok())
.collect()
})
}
static ACTIVE: OnceLock<BTreeMap<String, String>> = OnceLock::new();
fn merge(code: &str) -> BTreeMap<String, String> {
let mut merged = parsed()
.iter()
.find(|c| c.meta.code == constants::DEFAULT_LANGUAGE)
.map(|c| c.strings.clone())
.unwrap_or_default();
if code != constants::DEFAULT_LANGUAGE
&& let Some(chosen) = parsed().iter().find(|c| c.meta.code == code)
{
for (key, value) in &chosen.strings {
if !value.trim().is_empty() {
merged.insert(key.clone(), value.clone());
}
}
}
merged
}
pub fn init(configured: Option<&str>) {
let requested = std::env::var(constants::ENV_LANGUAGE)
.ok()
.map(|v| v.trim().to_string())
.filter(|v| !v.is_empty())
.or_else(|| configured.map(str::to_string))
.unwrap_or_else(|| constants::DEFAULT_LANGUAGE.to_string());
let code = if language(&requested).is_some() {
requested
} else {
constants::DEFAULT_LANGUAGE.to_string()
};
let _ = ACTIVE.set(merge(&code));
}
pub fn t(key: &'static str) -> &'static str {
ACTIVE
.get_or_init(|| merge(constants::DEFAULT_LANGUAGE))
.get(key)
.map(String::as_str)
.unwrap_or(key)
}
pub fn tf(key: &'static str, args: &[(&str, &str)]) -> String {
let mut out = t(key).to_string();
for (name, value) in args {
out = out.replace(&format!("{{{name}}}"), value);
}
out
}
pub fn language(code: &str) -> Option<&'static Meta> {
parsed()
.iter()
.find(|c| c.meta.code == code)
.map(|c| &c.meta)
}
pub fn choices() -> &'static [(&'static str, &'static str)] {
static CHOICES: OnceLock<Vec<(&'static str, &'static str)>> = OnceLock::new();
CHOICES.get_or_init(|| {
parsed()
.iter()
.map(|c| (c.meta.code.as_str(), c.meta.native_name.as_str()))
.collect()
})
}
pub fn catalogue_line() -> String {
choices()
.iter()
.map(|(code, native)| format!("{code} {native}"))
.collect::<Vec<_>>()
.join(" · ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn catalogues_all_parse() {
assert_eq!(
parsed().len(),
CATALOGUES.len(),
"a locale file failed to parse; every entry in CATALOGUES must be valid JSON \
with a _meta block"
);
}
#[test]
fn catalogues_cover_english() {
let english: Vec<&String> = parsed()
.iter()
.find(|c| c.meta.code == constants::DEFAULT_LANGUAGE)
.expect("en.json must exist")
.strings
.keys()
.collect();
for catalogue in parsed() {
for key in catalogue.strings.keys() {
assert!(
english.contains(&key),
"{}.json has key `{key}`, which en.json does not",
catalogue.meta.code
);
}
}
}
#[test]
fn codes_are_unique_and_start_with_english() {
let mut seen = std::collections::BTreeSet::new();
for (code, _) in choices() {
assert!(seen.insert(*code), "duplicate language code `{code}`");
}
assert_eq!(
choices().first().map(|(code, _)| *code),
Some(constants::DEFAULT_LANGUAGE)
);
}
#[test]
fn every_catalogue_names_itself() {
for (code, _) in choices() {
let meta = language(code).expect("choices() only lists catalogues that parsed");
assert!(!meta.english_name.trim().is_empty(), "{code}");
assert!(!meta.native_name.trim().is_empty(), "{code}");
}
}
#[test]
fn unknown_language_is_not_supported() {
assert!(language("en").is_some());
assert!(language("te").is_some());
assert!(language("xx").is_none());
assert!(language("EN").is_none());
}
#[test]
fn placeholders_are_filled_by_name() {
let filled = tf("run.freed", &[("size", "1.2 GB"), ("count", "7")]);
assert!(filled.contains("1.2 GB"), "{filled}");
assert!(filled.contains('7'), "{filled}");
assert!(!filled.contains('{'), "{filled}");
}
#[test]
fn an_untranslated_key_falls_back_to_english() {
let english = merge(constants::DEFAULT_LANGUAGE);
for (code, _) in choices() {
for (key, value) in merge(code) {
assert!(!value.trim().is_empty(), "`{code}` has an empty `{key}`");
assert!(
english.contains_key(&key),
"`{code}` invented the key `{key}`"
);
}
}
}
}