use std::collections::HashMap;
use std::sync::{LazyLock, Mutex};
struct TranslationRegistry {
locale: String,
translations: HashMap<(String, String), String>,
}
static REGISTRY: LazyLock<Mutex<TranslationRegistry>> = LazyLock::new(|| {
Mutex::new(TranslationRegistry {
locale: String::new(),
translations: HashMap::new(),
})
});
pub fn register_translation(locale: &str, key: &str, value: &str) {
let mut reg = REGISTRY.lock().unwrap_or_else(|e| e.into_inner());
reg.translations
.insert((locale.to_string(), key.to_string()), value.to_string());
}
pub fn set_locale(locale: &str) {
let mut reg = REGISTRY.lock().unwrap_or_else(|e| e.into_inner());
reg.locale = locale.to_string();
}
pub fn get_locale() -> String {
let reg = REGISTRY.lock().unwrap_or_else(|e| e.into_inner());
if reg.locale.is_empty() {
"en".to_string()
} else {
reg.locale.clone()
}
}
pub fn translate_or_fallback(default: &str, i18n_key: Option<&str>) -> String {
let key = match i18n_key {
Some(k) if !k.is_empty() => k,
_ => return default.to_string(),
};
let locale = {
let reg = REGISTRY.lock().unwrap_or_else(|e| e.into_inner());
if reg.locale.is_empty() {
return default.to_string();
}
reg.locale.clone()
};
let reg = REGISTRY.lock().unwrap_or_else(|e| e.into_inner());
reg.translations
.get(&(locale, key.to_string()))
.cloned()
.unwrap_or_else(|| default.to_string())
}
pub fn clear_translations() {
let mut reg = REGISTRY.lock().unwrap_or_else(|e| e.into_inner());
reg.translations.clear();
reg.locale.clear();
}
#[cfg(feature = "i18n")]
#[cfg(test)]
use std::cmp::Ordering;
#[cfg(feature = "i18n")]
use icu::collator::CollatorBorrowed;
#[cfg(feature = "i18n")]
use icu::decimal::DecimalFormatter;
#[cfg(feature = "i18n")]
use icu::locale::Locale;
#[cfg(feature = "i18n")]
use icu::plurals::PluralRules;
#[cfg(feature = "i18n")]
use thiserror::Error;
#[cfg(feature = "i18n")]
pub(crate) const DEFAULT_Q_VALUE: f64 = 1.0;
#[cfg(feature = "i18n")]
mod i18n_impl;
#[cfg(feature = "i18n")]
pub use i18n_impl::parse_accept_language;
#[cfg(feature = "i18n")]
#[derive(Debug, Error)]
pub enum I18nError {
#[error("invalid locale '{input}': {reason}")]
InvalidLocale {
input: String,
reason: String,
},
#[error("invalid number '{input}': {reason}")]
InvalidNumber {
input: String,
reason: String,
},
#[error("date error: {0}")]
DateError(String),
#[error("formatting error: {0}")]
FormatError(String),
#[error("no valid locale found in Accept-Language header '{header}'")]
NoValidLocale {
header: String,
},
}
#[cfg(feature = "i18n")]
pub struct HttpI18nFormatter {
locale: Locale,
decimal_formatter: DecimalFormatter,
plural_rules: PluralRules,
collator: CollatorBorrowed<'static>,
}
#[cfg(test)]
mod translation_tests {
use super::*;
static REGISTRY_LOCK: Mutex<()> = Mutex::new(());
fn registry_guard() -> std::sync::MutexGuard<'static, ()> {
REGISTRY_LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
#[test]
fn test_translate_or_fallback_no_key() {
assert_eq!(translate_or_fallback("default text", None), "default text");
}
#[test]
fn test_translate_or_fallback_empty_key() {
assert_eq!(
translate_or_fallback("default text", Some("")),
"default text"
);
}
#[test]
fn test_translate_or_fallback_no_locale_set() {
let _guard = registry_guard();
clear_translations();
assert_eq!(
translate_or_fallback("default text", Some("some.key")),
"default text"
);
}
#[test]
fn test_translate_or_fallback_with_translation() {
let _guard = registry_guard();
clear_translations();
register_translation("zh-CN", "forge.embed", "生成嵌入向量");
set_locale("zh-CN");
assert_eq!(
translate_or_fallback("Generate embedding", Some("forge.embed")),
"生成嵌入向量"
);
clear_translations();
}
#[test]
fn test_translate_or_fallback_missing_translation() {
let _guard = registry_guard();
clear_translations();
set_locale("zh-CN");
assert_eq!(
translate_or_fallback("Generate embedding", Some("forge.nonexistent")),
"Generate embedding"
);
clear_translations();
}
#[test]
fn test_translate_or_fallback_wrong_locale() {
let _guard = registry_guard();
clear_translations();
register_translation("zh-CN", "forge.embed", "生成嵌入向量");
set_locale("ja-JP");
assert_eq!(
translate_or_fallback("Generate embedding", Some("forge.embed")),
"Generate embedding"
);
clear_translations();
}
#[test]
fn test_set_and_get_locale() {
let _guard = registry_guard();
clear_translations();
assert_eq!(get_locale(), "en"); set_locale("zh-CN");
assert_eq!(get_locale(), "zh-CN");
clear_translations();
}
#[test]
fn test_clear_translations() {
let _guard = registry_guard();
register_translation("en", "key1", "value1");
set_locale("en");
assert_eq!(translate_or_fallback("default", Some("key1")), "value1");
clear_translations();
assert_eq!(translate_or_fallback("default", Some("key1")), "default");
assert_eq!(get_locale(), "en"); }
#[test]
fn test_multiple_locales() {
let _guard = registry_guard();
clear_translations();
register_translation("zh-CN", "greet", "你好");
register_translation("ja-JP", "greet", "こんにちは");
set_locale("zh-CN");
assert_eq!(translate_or_fallback("Hello", Some("greet")), "你好");
set_locale("ja-JP");
assert_eq!(translate_or_fallback("Hello", Some("greet")), "こんにちは");
clear_translations();
}
}
#[cfg(all(test, feature = "i18n"))]
mod tests {
use super::*;
#[test]
fn test_locale_parsing_en() {
let fmt = HttpI18nFormatter::new("en-US");
assert!(fmt.is_ok(), "en-US should parse successfully");
}
#[test]
fn test_locale_parsing_zh() {
let fmt = HttpI18nFormatter::new("zh-CN");
assert!(fmt.is_ok(), "zh-CN should parse successfully");
}
#[test]
fn test_invalid_locale() {
let result = HttpI18nFormatter::new("not-a-valid-locale!!!");
assert!(result.is_err(), "invalid locale should return error");
match result.err().unwrap() {
I18nError::InvalidLocale { input, .. } => assert_eq!(input, "not-a-valid-locale!!!"),
other => panic!("expected InvalidLocale, got {other:?}"),
}
}
#[test]
fn test_parse_accept_language() {
let locales = parse_accept_language("en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7");
assert_eq!(
locales,
vec!["en-US", "en", "zh-CN", "zh"],
"locales should be sorted by q-value descending: got {locales:?}"
);
}
#[test]
fn test_parse_accept_language_default_q() {
let locales = parse_accept_language("fr,en;q=0.9");
assert_eq!(
locales,
vec!["fr", "en"],
"entry without q= should get default 1.0: got {locales:?}"
);
}
#[test]
fn test_parse_accept_language_q_zero_excluded() {
let locales = parse_accept_language("en;q=0,fr");
assert_eq!(
locales,
vec!["fr"],
"q=0 entries should be excluded: got {locales:?}"
);
}
#[test]
fn test_parse_accept_language_empty() {
let locales = parse_accept_language("");
assert!(locales.is_empty(), "empty header should return empty vec");
}
#[test]
fn test_from_accept_language() {
let fmt = HttpI18nFormatter::from_accept_language("en-US,en;q=0.9,zh-CN;q=0.8");
assert!(fmt.is_ok(), "should create formatter from valid header");
}
#[test]
fn test_from_accept_language_fallback() {
let fmt = HttpI18nFormatter::from_accept_language("not-a-locale!!!,en-US");
assert!(fmt.is_ok(), "should fall back to valid locale");
}
#[test]
fn test_from_accept_language_all_invalid() {
let result = HttpI18nFormatter::from_accept_language("not-a-locale!!!");
assert!(result.is_err(), "all-invalid header should error");
match result.err().unwrap() {
I18nError::NoValidLocale { header, .. } => {
assert_eq!(header, "not-a-locale!!!");
}
other => panic!("expected NoValidLocale, got {other:?}"),
}
}
#[test]
fn test_format_error_message_singular() {
let fmt = HttpI18nFormatter::new("en").expect("en locale");
let msg = fmt.format_error_message(404, 1).expect("error message");
assert!(
msg.contains("One"),
"count=1 should contain plural category One: got '{msg}'"
);
assert!(
msg.contains("error"),
"singular form should use 'error': got '{msg}'"
);
assert!(
msg.contains("404"),
"message should contain status code: got '{msg}'"
);
}
#[test]
fn test_format_error_message_plural() {
let fmt = HttpI18nFormatter::new("en").expect("en locale");
let msg = fmt.format_error_message(404, 2).expect("error message");
assert!(
msg.contains("Other"),
"count=2 should contain plural category Other: got '{msg}'"
);
assert!(
msg.contains("errors"),
"plural form should use 'errors': got '{msg}'"
);
}
#[test]
fn test_format_number_en() {
let fmt = HttpI18nFormatter::new("en-US").expect("en-US locale");
let result = fmt.format_number(1_234_567.89_f64).expect("format number");
assert!(
result.contains(','),
"en-US number should contain thousands separator: got '{result}'"
);
assert!(
result.contains('.'),
"en-US number should contain decimal point: got '{result}'"
);
}
#[test]
fn test_format_number_not_finite() {
let fmt = HttpI18nFormatter::new("en-US").expect("en-US locale");
assert!(fmt.format_number(f64::NAN).is_err());
assert!(fmt.format_number(f64::INFINITY).is_err());
}
#[test]
fn test_format_timestamp() {
let fmt = HttpI18nFormatter::new("en-US").expect("en-US locale");
let result = fmt.format_timestamp(2026, 7, 11).expect("format timestamp");
assert!(
result.contains("2026"),
"timestamp should contain year: got '{result}'"
);
assert!(
!result.is_empty(),
"timestamp should be non-empty: got '{result}'"
);
}
#[test]
fn test_compare_headers() {
let fmt = HttpI18nFormatter::new("en").expect("en locale");
assert_eq!(
fmt.compare_headers("apple", "banana").expect("compare"),
Ordering::Less,
"apple < banana"
);
assert_eq!(
fmt.compare_headers("banana", "apple").expect("compare"),
Ordering::Greater,
"banana > apple"
);
assert_eq!(
fmt.compare_headers("apple", "apple").expect("compare"),
Ordering::Equal,
"apple == apple"
);
}
#[test]
fn test_format_timestamp_invalid_date() {
let fmt = HttpI18nFormatter::new("en-US").expect("en-US locale");
assert!(
fmt.format_timestamp(2026, 13, 1).is_err(),
"month=13 should return date error"
);
assert!(
fmt.format_timestamp(2026, 2, 30).is_err(),
"Feb 30 should return date error"
);
}
#[test]
fn test_parse_accept_language_whitespace_entries() {
let locales = parse_accept_language(" , en ; q=0.9 , ");
assert_eq!(
locales,
vec!["en"],
"should handle whitespace-only and trimmed entries: got {locales:?}"
);
}
#[test]
fn test_parse_accept_language_malformed_q() {
let locales = parse_accept_language("fr;q=abc,en;q=0.5");
assert_eq!(
locales,
vec!["fr", "en"],
"malformed q= should fall back to default 1.0: got {locales:?}"
);
}
#[test]
fn test_parse_accept_language_q_zero_mixed() {
let locales = parse_accept_language("en;q=0,fr;q=0.5,de");
assert_eq!(
locales,
vec!["de", "fr"],
"q=0 excluded, others sorted by q: got {locales:?}"
);
}
}