pub use rust_i18n::t;
pub fn set_locale(locale: &str) {
rust_i18n::set_locale(locale);
}
pub fn current_locale() -> String {
rust_i18n::locale().to_string()
}
pub const AVAILABLE_LOCALES: &[&str] = &["en", "zh-CN", "zh-TW"];
pub const DEFAULT_LOCALE: &str = "en";
pub fn init_locale_from_env() {
if let Ok(lang) = std::env::var("DEFAULT_LOCALE") {
let locale = normalize_locale(&lang);
if AVAILABLE_LOCALES.contains(&locale.as_str()) {
set_locale(&locale);
return;
} else {
tracing::warn!(
"Invalid locale '{}' from DEFAULT_LOCALE, falling back. Supported: {:?}",
locale,
AVAILABLE_LOCALES
);
}
}
if let Ok(lang) = std::env::var("LANG") {
let locale = parse_lang_env(&lang);
if AVAILABLE_LOCALES.contains(&locale.as_str()) {
set_locale(&locale);
return;
}
}
set_locale(DEFAULT_LOCALE);
}
fn normalize_locale(input: &str) -> String {
let input = input.trim();
let input = input.split('.').next().unwrap_or(input);
let input = input.split('@').next().unwrap_or(input);
match input.to_lowercase().as_str() {
"en" | "en_us" | "en-us" | "en_gb" | "en-gb" => return "en".to_string(),
"zh-cn" | "zh_cn" | "zh-hans" => return "zh-CN".to_string(),
"zh-tw" | "zh_tw" | "zh-hant" => return "zh-TW".to_string(),
"zh" => return "zh-CN".to_string(), _ => {}
}
let parts: Vec<&str> = input.split(|c| c == '-' || c == '_').collect();
if parts.len() >= 2 {
let lang = parts[0].to_lowercase();
let region = parts[1].to_uppercase();
if lang == "en" {
return "en".to_string();
}
return format!("{}-{}", lang, region);
}
input.to_string()
}
fn parse_lang_env(lang: &str) -> String {
let lang = lang.split('.').next().unwrap_or(lang);
let lang = lang.split('@').next().unwrap_or(lang);
normalize_locale(lang)
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Mutex, OnceLock};
fn test_lock() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
}
struct EnvRestore {
saved: Vec<(&'static str, Option<String>)>,
}
impl Drop for EnvRestore {
fn drop(&mut self) {
for (key, value) in &self.saved {
match value {
Some(v) => unsafe { std::env::set_var(key, v) },
None => unsafe { std::env::remove_var(key) },
}
}
}
}
fn prepare_env(overrides: &[(&'static str, Option<&str>)]) -> EnvRestore {
let tracked_keys = ["DEFAULT_LOCALE", "LANG", "MCP_PROXY_LANG", "APP_LANG"];
let mut saved = Vec::with_capacity(tracked_keys.len());
for key in tracked_keys {
saved.push((key, std::env::var(key).ok()));
unsafe { std::env::remove_var(key) };
}
for (key, value) in overrides {
match value {
Some(v) => unsafe { std::env::set_var(key, v) },
None => unsafe { std::env::remove_var(key) },
}
}
EnvRestore { saved }
}
#[test]
fn test_normalize_locale() {
let _guard = test_lock().lock().expect("locale test lock poisoned");
assert_eq!(normalize_locale("en"), "en");
assert_eq!(normalize_locale("EN"), "en");
assert_eq!(normalize_locale("zh-CN"), "zh-CN");
assert_eq!(normalize_locale("zh-cn"), "zh-CN");
assert_eq!(normalize_locale("zh_CN"), "zh-CN");
assert_eq!(normalize_locale("zh-TW"), "zh-TW");
assert_eq!(normalize_locale("zh_tw"), "zh-TW");
assert_eq!(normalize_locale("zh"), "zh-CN");
assert_eq!(normalize_locale("en_US.UTF-8"), "en");
assert_eq!(normalize_locale("zh_CN@cjk"), "zh-CN");
}
#[test]
fn test_parse_lang_env() {
let _guard = test_lock().lock().expect("locale test lock poisoned");
assert_eq!(parse_lang_env("en_US.UTF-8"), "en");
assert_eq!(parse_lang_env("zh_CN.UTF-8"), "zh-CN");
assert_eq!(parse_lang_env("zh_TW.UTF-8"), "zh-TW");
assert_eq!(parse_lang_env("zh_CN"), "zh-CN");
assert_eq!(parse_lang_env("en_US@cjk"), "en");
}
#[test]
fn test_set_and_get_locale() {
let _guard = test_lock().lock().expect("locale test lock poisoned");
set_locale("zh-CN");
assert_eq!(current_locale(), "zh-CN");
set_locale("en");
assert_eq!(current_locale(), "en");
set_locale("zh-TW");
assert_eq!(current_locale(), "zh-TW");
}
#[test]
fn test_translation_completeness() {
let _guard = test_lock().lock().expect("locale test lock poisoned");
set_locale("en");
let test_msg = t!("common.success").to_string();
assert_ne!(
test_msg, "common.success",
"Translations are not loaded; expected crate-local locales to be available"
);
let critical_keys = [
"errors.mcp_proxy.service_not_found",
"errors.mcp_proxy.service_startup_failed",
"errors.document_parser.config",
"errors.document_parser.parse",
"errors.oss.config",
"errors.oss.network",
"errors.voice.config",
"errors.voice.transcription",
"cli.startup.service_starting",
"cli.startup.success",
"common.error",
"common.success",
];
for locale in AVAILABLE_LOCALES {
set_locale(locale);
for key in &critical_keys {
let msg = match *key {
"errors.mcp_proxy.service_not_found" => {
t!("errors.mcp_proxy.service_not_found", service = "test").to_string()
}
"errors.mcp_proxy.service_startup_failed" => t!(
"errors.mcp_proxy.service_startup_failed",
mcp_id = "test",
reason = "test"
)
.to_string(),
_ => t!(*key).to_string(),
};
assert_ne!(
msg, *key,
"Missing translation for '{}' in locale '{}'",
key, locale
);
}
}
}
#[test]
fn test_all_locales_available() {
let _guard = test_lock().lock().expect("locale test lock poisoned");
for locale in AVAILABLE_LOCALES {
assert!(
locale.contains('-') || *locale == "en",
"Locale '{}' should follow language-region format",
locale
);
set_locale(locale);
}
set_locale(DEFAULT_LOCALE);
}
#[test]
fn test_init_locale_from_env_prefers_default_locale() {
let _guard = test_lock().lock().expect("locale test lock poisoned");
let _env = prepare_env(&[
("DEFAULT_LOCALE", Some("zh-TW")),
("LANG", Some("en_US.UTF-8")),
("MCP_PROXY_LANG", Some("zh-CN")),
("APP_LANG", Some("zh-CN")),
]);
init_locale_from_env();
assert_eq!(current_locale(), "zh-TW");
set_locale(DEFAULT_LOCALE);
}
#[test]
fn test_init_locale_from_env_falls_back_to_lang() {
let _guard = test_lock().lock().expect("locale test lock poisoned");
let _env = prepare_env(&[
("DEFAULT_LOCALE", Some("unsupported")),
("LANG", Some("zh_CN.UTF-8")),
]);
init_locale_from_env();
assert_eq!(current_locale(), "zh-CN");
set_locale(DEFAULT_LOCALE);
}
#[test]
fn test_init_locale_from_env_falls_back_to_english() {
let _guard = test_lock().lock().expect("locale test lock poisoned");
let _env = prepare_env(&[
("DEFAULT_LOCALE", Some("unsupported")),
("LANG", Some("ja_JP.UTF-8")),
]);
init_locale_from_env();
assert_eq!(current_locale(), "en");
set_locale(DEFAULT_LOCALE);
}
#[test]
fn test_init_locale_from_env_ignores_removed_env_vars() {
let _guard = test_lock().lock().expect("locale test lock poisoned");
let _env = prepare_env(&[
("MCP_PROXY_LANG", Some("zh-TW")),
("APP_LANG", Some("zh-CN")),
]);
init_locale_from_env();
assert_eq!(current_locale(), "en");
set_locale(DEFAULT_LOCALE);
}
}