use std::collections::BTreeMap;
use serde_json::Value;
use crate::ui::{self, RESET, YELLOW, col};
pub(crate) fn resolve_fallback(
messages: &BTreeMap<String, Value>,
locales: &[String],
) -> BTreeMap<String, Value> {
let mut all_keys = BTreeMap::<String, ()>::new();
for data in messages.values() {
if let Some(obj) = data.as_object() {
for key in obj.keys() {
all_keys.insert(key.clone(), ());
}
}
}
let mut resolved = BTreeMap::new();
for locale in locales {
let mut locale_msgs = serde_json::Map::new();
for key in all_keys.keys() {
let val = lookup_value(messages, locale, key, locales);
match val {
Some(v) => {
locale_msgs.insert(key.clone(), v);
}
None => {
ui::detail(&format!(
"{}warning{}: i18n key \"{key}\" has no value in any locale, using key as fallback",
col(YELLOW),
col(RESET)
));
locale_msgs.insert(key.clone(), Value::String(key.clone()));
}
}
}
resolved.insert(locale.clone(), Value::Object(locale_msgs));
}
resolved
}
fn lookup_value(
messages: &BTreeMap<String, Value>,
target_locale: &str,
key: &str,
locales: &[String],
) -> Option<Value> {
if let Some(val) = get_non_empty(messages, target_locale, key) {
return Some(val);
}
for locale in locales {
if locale == target_locale {
continue;
}
if let Some(val) = get_non_empty(messages, locale, key) {
return Some(val);
}
}
None
}
fn get_non_empty(messages: &BTreeMap<String, Value>, locale: &str, key: &str) -> Option<Value> {
let data = messages.get(locale)?;
let val = data.get(key)?;
if let Some(s) = val.as_str()
&& s.is_empty()
{
return None;
}
Some(val.clone())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn make_messages(pairs: &[(&str, Value)]) -> BTreeMap<String, Value> {
pairs.iter().map(|(k, v)| (k.to_string(), v.clone())).collect()
}
#[test]
fn full_coverage() {
let msgs = make_messages(&[
("en", json!({"hello": "Hello", "bye": "Bye"})),
("zh", json!({"hello": "你好", "bye": "再见"})),
]);
let resolved = resolve_fallback(&msgs, &["en".into(), "zh".into()]);
assert_eq!(resolved["en"]["hello"], "Hello");
assert_eq!(resolved["zh"]["hello"], "你好");
}
#[test]
fn fallback_to_other_locale() {
let msgs = make_messages(&[
("en", json!({"hello": "Hello", "bye": "Bye"})),
("zh", json!({"hello": "你好"})),
]);
let resolved = resolve_fallback(&msgs, &["en".into(), "zh".into()]);
assert_eq!(resolved["zh"]["bye"], "Bye");
assert_eq!(resolved["en"]["bye"], "Bye");
}
#[test]
fn fallback_to_key_itself() {
let msgs = make_messages(&[
("en", json!({"hello": "Hello"})),
("zh", json!({"hello": "你好", "missing.key": ""})),
]);
let locales = vec!["en".into(), "zh".into()];
let resolved = resolve_fallback(&msgs, &locales);
assert_eq!(resolved["en"]["missing.key"], "missing.key");
assert_eq!(resolved["zh"]["missing.key"], "missing.key");
}
#[test]
fn empty_string_treated_as_missing() {
let msgs = make_messages(&[("en", json!({"hello": "Hello"})), ("zh", json!({"hello": ""}))]);
let resolved = resolve_fallback(&msgs, &["en".into(), "zh".into()]);
assert_eq!(resolved["zh"]["hello"], "Hello");
}
#[test]
fn three_locales_walk_order() {
let msgs =
make_messages(&[("en", json!({})), ("zh", json!({"greeting": "你好"})), ("ja", json!({}))]);
let resolved = resolve_fallback(&msgs, &["en".into(), "zh".into(), "ja".into()]);
assert_eq!(resolved["en"]["greeting"], "你好");
assert_eq!(resolved["ja"]["greeting"], "你好");
}
}