use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
pub fn resolve_i18n_dir(bundle_root: &Path) -> Option<PathBuf> {
if let Ok(dir) = std::env::var("GREENTIC_PROVIDER_I18N_DIR") {
let p = PathBuf::from(dir);
if p.is_dir() {
return Some(p);
}
}
let mut ancestor = bundle_root.parent();
for _ in 0..4 {
let Some(dir) = ancestor else { break };
let sibling = dir.join("greentic-i18n").join("i18n").join("providers");
if sibling.is_dir() {
return Some(sibling);
}
ancestor = dir.parent();
}
let inside = bundle_root.join("i18n").join("providers");
if inside.is_dir() {
return Some(inside);
}
None
}
pub fn load_locale_file(dir: &Path, locale: &str) -> BTreeMap<String, String> {
let path = dir.join(format!("{locale}.json"));
let Ok(data) = std::fs::read_to_string(&path) else {
return BTreeMap::new();
};
let Ok(obj) = serde_json::from_str::<serde_json::Value>(&data) else {
return BTreeMap::new();
};
obj.as_object()
.map(|m| {
m.iter()
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
.collect()
})
.unwrap_or_default()
}
pub fn load_and_merge(
wasm_english: &BTreeMap<String, String>,
locale: &str,
dir: Option<&Path>,
) -> BTreeMap<String, String> {
let Some(dir) = dir else {
return wasm_english.clone();
};
let disk_base = load_locale_file(dir, "en");
let locale_map = if locale != "en" {
Some(load_locale_file(dir, locale))
} else {
None
};
merge_i18n_layers_local(disk_base, wasm_english, locale_map.as_ref())
}
fn merge_i18n_layers_local(
mut disk_base: BTreeMap<String, String>,
wasm_english: &BTreeMap<String, String>,
locale_map: Option<&BTreeMap<String, String>>,
) -> BTreeMap<String, String> {
for (key, value) in wasm_english {
disk_base.insert(key.clone(), value.clone());
}
if let Some(locale) = locale_map {
for (key, value) in locale {
if disk_base.contains_key(key) {
disk_base.insert(key.clone(), value.clone());
}
}
}
disk_base
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn load_and_merge_overlays_locale() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
fs::write(
dir.join("en.json"),
r#"{"telegram.qa.setup.bot_token":"Bot token","telegram.qa.setup.enabled":"Enable provider"}"#,
)
.unwrap();
fs::write(
dir.join("id.json"),
r#"{"telegram.qa.setup.bot_token":"Token bot","unknown.key":"ignored"}"#,
)
.unwrap();
let wasm_english = BTreeMap::new(); let merged = load_and_merge(&wasm_english, "id", Some(dir));
assert_eq!(merged["telegram.qa.setup.bot_token"], "Token bot");
assert_eq!(merged["telegram.qa.setup.enabled"], "Enable provider");
assert!(!merged.contains_key("unknown.key"));
}
#[test]
fn load_and_merge_uses_disk_en_when_wasm_empty() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
fs::write(dir.join("en.json"), r#"{"key":"disk value"}"#).unwrap();
let merged = load_and_merge(&BTreeMap::new(), "en", Some(dir));
assert_eq!(merged["key"], "disk value");
}
#[test]
fn load_and_merge_wasm_overrides_disk() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
fs::write(dir.join("en.json"), r#"{"key":"from disk"}"#).unwrap();
let mut wasm = BTreeMap::new();
wasm.insert("key".into(), "from WASM".into());
let merged = load_and_merge(&wasm, "en", Some(dir));
assert_eq!(merged["key"], "from WASM");
}
#[test]
fn load_and_merge_returns_wasm_when_no_dir() {
let mut wasm = BTreeMap::new();
wasm.insert("key".into(), "value".into());
let merged = load_and_merge(&wasm, "id", None);
assert_eq!(merged, wasm);
}
#[test]
fn resolve_i18n_dir_from_env() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path().to_path_buf();
unsafe { std::env::set_var("GREENTIC_PROVIDER_I18N_DIR", &dir) };
let result = resolve_i18n_dir(Path::new("/nonexistent"));
unsafe { std::env::remove_var("GREENTIC_PROVIDER_I18N_DIR") };
assert_eq!(result, Some(dir));
}
}