提交qiming-mcp-proxy

This commit is contained in:
Codex
2026-06-01 13:03:20 +08:00
parent 9e9486b7c2
commit afb3d9f4e6
394 changed files with 124494 additions and 0 deletions

View File

@@ -0,0 +1,391 @@
//! 国际化模块
//!
//! 使用 rust-i18n 提供多语言支持,支持中文简体、中文繁体、英文三种语言。
//!
//! # 使用方法
//!
//! ```rust,ignore
//! use mcp_common::{t, set_locale, init_locale_from_env};
//!
//! // 初始化语言设置(通常在程序启动时调用)
//! init_locale_from_env();
//!
//! // 获取翻译
//! let msg = t!("errors.mcp_proxy.service_not_found", service = "my-service");
//!
//! // 手动设置语言
//! set_locale("zh-CN");
//! ```
//!
//! # 支持的语言
//!
//! - `en` - English
//! - `zh-CN` - 中文简体
//! - `zh-TW` - 中文繁体
//!
//! # 配置优先级
//!
//! 1. `DEFAULT_LOCALE` 环境变量(最高优先级)
//! 2. `LANG` 系统环境变量
//! 3. 默认使用英文
//!
//! # 线程安全
//!
//! `set_locale()` 和 `init_locale_from_env()` 应在程序启动时调用。
//! 语言设置是全局状态,不建议在运行时多线程环境中修改。
// 注意: rust-i18n 的 i18n! 宏需要在 lib.rs 中调用
/// 导出 t! 宏,用于获取翻译
pub use rust_i18n::t;
/// 设置当前语言
///
/// # 线程安全
///
/// 此函数应在程序启动时调用,不建议在运行时多线程环境中调用。
/// 语言设置是全局状态,并发调用可能导致不一致的翻译结果。
///
/// # 示例
///
/// ```rust
/// use mcp_common::set_locale;
///
/// set_locale("zh-CN");
/// set_locale("en");
/// set_locale("zh-TW");
/// ```
pub fn set_locale(locale: &str) {
rust_i18n::set_locale(locale);
}
/// 获取当前语言设置
///
/// # 示例
///
/// ```rust
/// use mcp_common::current_locale;
///
/// let locale = current_locale();
/// println!("Current 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";
/// 从环境变量初始化语言设置
///
/// 按照以下优先级设置语言:
/// 1. `DEFAULT_LOCALE` 环境变量(最高优先级)
/// 2. `LANG` 系统环境变量(自动解析语言代码)
/// 3. 默认使用英文
///
/// # 示例
///
/// ```rust
/// use mcp_common::init_locale_from_env;
///
/// // 在程序启动时调用
/// init_locale_from_env();
/// ```
pub fn init_locale_from_env() {
// 优先使用 DEFAULT_LOCALE 环境变量(最高优先级)
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
);
}
}
// 其次尝试从 LANG 环境变量解析
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);
}
/// 标准化语言代码
///
/// 支持的输入格式:
/// - `en`, `EN`, `En`, `en_US`, `en_US.UTF-8` -> `en`
/// - `zh-CN`, `zh-cn`, `ZH-CN` -> `zh-CN`
/// - `zh_TW`, `zh-TW` -> `zh-TW`
/// - `zh`, `ZH` -> `zh-CN` (默认简体中文)
fn normalize_locale(input: &str) -> String {
let input = input.trim();
// 支持解析带编码/修饰符的值(例如 en_US.UTF-8、zh_CN@cjk
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();
// 英文变体统一映射到 en
if lang == "en" {
return "en".to_string();
}
return format!("{}-{}", lang, region);
}
input.to_string()
}
/// 解析 LANG 环境变量
///
/// 支持的格式:
/// - `en_US.UTF-8` -> `en`
/// - `zh_CN.UTF-8` -> `zh-CN`
/// - `zh_TW.UTF-8` -> `zh-TW`
/// - `zh_CN` -> `zh-CN`
fn parse_lang_env(lang: &str) -> String {
// 移除编码部分 (如 .UTF-8)
let lang = lang.split('.').next().unwrap_or(lang);
// 移除修饰部分 (如 @cjk)
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(),
};
// 翻译不应该返回 key 本身(表示翻译缺失)
assert_ne!(
msg, *key,
"Missing translation for '{}' in locale '{}'",
key, locale
);
}
}
}
/// 测试所有支持的语言都能正确切换
///
/// 注意:此测试依赖于 rust-i18n 的全局状态,在测试环境中可能不稳定。
/// 但在实际运行时,语言切换功能是正常的。
#[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);
}
}