Skip to main content

sz_orm_core/
i18n.rs

1//! # 国际化(i18n)支持
2//!
3//! 提供错误消息和日志消息的多语言框架。
4//!
5//! ## 设计目标
6//!
7//! - **向后兼容**:默认中文消息,不破坏现有 API
8//! - **可选启用**:使用方可注册自定义语言包
9//! - **零开销**:未注册语言包时直接返回默认消息
10//! - **线程安全**:使用 `RwLock` 保护语言目录
11//!
12//! ## 使用示例
13//!
14//! ```rust,ignore
15//! use sz_orm_core::i18n::{MessageCatalog, MessageKey, set_catalog, translate};
16//!
17//! // 1. 注册英文语言包
18//! let mut catalog = MessageCatalog::new();
19//! catalog.insert(MessageKey::ConnectionFailed, "Connection failed: {0}");
20//! set_catalog(catalog);
21//!
22//! // 2. 翻译消息(无注册时返回默认中文)
23//! let msg = translate(MessageKey::ConnectionFailed, &["timeout"]);
24//! ```
25//!
26//! ## 当前状态
27//!
28//! - 提供 `MessageKey` 枚举覆盖核心错误类型
29//! - 默认中文消息硬编码在 `MessageKey::default_msg()` 中
30//! - 使用方可通过 `set_catalog()` 注册自定义翻译
31//! - 后续版本将逐步迁移现有中文硬编码消息到 `MessageKey`
32
33use std::collections::HashMap;
34use std::sync::{OnceLock, RwLock};
35
36/// 消息键枚举
37///
38/// 覆盖 sz-orm-core 的核心错误类型。
39/// 后续版本将逐步扩展。
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub enum MessageKey {
42    /// 连接失败
43    ConnectionFailed,
44    /// 连接超时
45    ConnectionTimeout,
46    /// 查询错误
47    QueryError,
48    /// 未找到
49    NotFound,
50    /// 约束违反
51    ConstraintViolation,
52    /// 连接池耗尽
53    PoolExhausted,
54    /// 连接池超时
55    PoolTimeout,
56    /// 事务未启动
57    TxNotStarted,
58    /// 事务提交失败
59    TxCommitFailed,
60    /// 事务回滚失败
61    TxRollbackFailed,
62    /// 缓存未命中
63    CacheMiss,
64    /// 缓存写入失败
65    CacheWriteFailed,
66    /// SQL 注入检测
67    SqlInjectionDetected,
68    /// 参数绑定缺失
69    MissingParameter,
70    /// 类型转换失败
71    TypeMismatch,
72    /// 自定义消息(向后兼容)
73    Custom,
74}
75
76impl MessageKey {
77    /// 获取默认中文消息
78    pub fn default_msg(self) -> &'static str {
79        match self {
80            MessageKey::ConnectionFailed => "连接失败",
81            MessageKey::ConnectionTimeout => "连接超时",
82            MessageKey::QueryError => "查询错误",
83            MessageKey::NotFound => "未找到",
84            MessageKey::ConstraintViolation => "约束违反",
85            MessageKey::PoolExhausted => "连接池耗尽",
86            MessageKey::PoolTimeout => "连接池超时",
87            MessageKey::TxNotStarted => "事务未启动",
88            MessageKey::TxCommitFailed => "事务提交失败",
89            MessageKey::TxRollbackFailed => "事务回滚失败",
90            MessageKey::CacheMiss => "缓存未命中",
91            MessageKey::CacheWriteFailed => "缓存写入失败",
92            MessageKey::SqlInjectionDetected => "检测到 SQL 注入",
93            MessageKey::MissingParameter => "参数绑定缺失",
94            MessageKey::TypeMismatch => "类型转换失败",
95            MessageKey::Custom => "",
96        }
97    }
98}
99
100/// 消息目录(语言包)
101///
102/// 存储 `MessageKey` 到翻译消息的映射。
103/// 翻译消息可包含 `{0}`、`{1}` 等位置占位符。
104pub type MessageCatalog = HashMap<MessageKey, String>;
105
106/// 全局消息目录(OnceLock + RwLock)
107static CATALOG: OnceLock<RwLock<MessageCatalog>> = OnceLock::new();
108
109/// 获取全局消息目录的只读锁
110fn catalog() -> &'static RwLock<MessageCatalog> {
111    CATALOG.get_or_init(|| RwLock::new(MessageCatalog::new()))
112}
113
114/// 设置全局消息目录
115///
116/// 覆盖现有目录。通常在应用启动时调用一次。
117pub fn set_catalog(new_catalog: MessageCatalog) {
118    let mut guard = catalog()
119        .write()
120        .unwrap_or_else(|poisoned| poisoned.into_inner());
121    *guard = new_catalog;
122}
123
124/// 注册单条翻译
125///
126/// 向现有目录添加或覆盖单条翻译。
127pub fn register(key: MessageKey, msg: impl Into<String>) {
128    let mut guard = catalog()
129        .write()
130        .unwrap_or_else(|poisoned| poisoned.into_inner());
131    guard.insert(key, msg.into());
132}
133
134/// 清空全局消息目录
135///
136/// 恢复默认中文消息。
137pub fn clear() {
138    let mut guard = catalog()
139        .write()
140        .unwrap_or_else(|poisoned| poisoned.into_inner());
141    guard.clear();
142}
143
144/// 翻译消息
145///
146/// 若目录中存在翻译,则使用翻译并用 `args` 替换 `{0}`、`{1}` 等占位符;
147/// 否则返回 `key.default_msg()`。
148pub fn translate(key: MessageKey, args: &[&str]) -> String {
149    let catalog = catalog().read().map(|g| g.clone()).unwrap_or_default();
150    if let Some(template) = catalog.get(&key) {
151        format_args(template, args)
152    } else {
153        key.default_msg().to_string()
154    }
155}
156
157/// 格式化占位符
158///
159/// 将 `{0}`、`{1}` 等替换为 `args` 中对应索引的字符串。
160/// 越界索引保留原占位符。
161fn format_args(template: &str, args: &[&str]) -> String {
162    let mut result = String::with_capacity(template.len());
163    let mut chars = template.chars().peekable();
164    while let Some(c) = chars.next() {
165        if c == '{' {
166            let mut idx_str = String::new();
167            while let Some(&next) = chars.peek() {
168                if next == '}' {
169                    chars.next();
170                    break;
171                }
172                idx_str.push(next);
173                chars.next();
174            }
175            if let Ok(idx) = idx_str.parse::<usize>() {
176                if let Some(arg) = args.get(idx) {
177                    result.push_str(arg);
178                } else {
179                    result.push('{');
180                    result.push_str(&idx_str);
181                    result.push('}');
182                }
183            } else {
184                result.push('{');
185                result.push_str(&idx_str);
186                result.push('}');
187            }
188        } else {
189            result.push(c);
190        }
191    }
192    result
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn test_default_message() {
201        assert_eq!(MessageKey::ConnectionFailed.default_msg(), "连接失败");
202        assert_eq!(MessageKey::QueryError.default_msg(), "查询错误");
203    }
204
205    #[test]
206    fn test_translate_default() {
207        clear();
208        let msg = translate(MessageKey::ConnectionFailed, &[]);
209        assert_eq!(msg, "连接失败");
210    }
211
212    #[test]
213    fn test_translate_with_catalog() {
214        clear();
215        let mut catalog = MessageCatalog::new();
216        catalog.insert(
217            MessageKey::ConnectionFailed,
218            "Connection failed: {0}".to_string(),
219        );
220        set_catalog(catalog);
221        let msg = translate(MessageKey::ConnectionFailed, &["timeout"]);
222        assert_eq!(msg, "Connection failed: timeout");
223        clear();
224    }
225
226    #[test]
227    fn test_register_single() {
228        clear();
229        register(MessageKey::NotFound, "Not found");
230        let msg = translate(MessageKey::NotFound, &[]);
231        assert_eq!(msg, "Not found");
232        clear();
233    }
234
235    #[test]
236    fn test_format_args_out_of_bounds() {
237        let result = format_args("Hello {0} {1}", &["world"]);
238        assert_eq!(result, "Hello world {1}");
239    }
240
241    #[test]
242    fn test_format_args_no_placeholders() {
243        let result = format_args("Hello world", &[]);
244        assert_eq!(result, "Hello world");
245    }
246
247    #[test]
248    fn test_format_args_invalid_index() {
249        let result = format_args("Hello {abc}", &[]);
250        assert_eq!(result, "Hello {abc}");
251    }
252
253    #[test]
254    fn test_clear() {
255        register(MessageKey::QueryError, "Query error");
256        assert_eq!(translate(MessageKey::QueryError, &[]), "Query error");
257        clear();
258        assert_eq!(translate(MessageKey::QueryError, &[]), "查询错误");
259    }
260}