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().write().expect("i18n catalog poisoned");
119    *guard = new_catalog;
120}
121
122/// 注册单条翻译
123///
124/// 向现有目录添加或覆盖单条翻译。
125pub fn register(key: MessageKey, msg: impl Into<String>) {
126    let mut guard = catalog().write().expect("i18n catalog poisoned");
127    guard.insert(key, msg.into());
128}
129
130/// 清空全局消息目录
131///
132/// 恢复默认中文消息。
133pub fn clear() {
134    let mut guard = catalog().write().expect("i18n catalog poisoned");
135    guard.clear();
136}
137
138/// 翻译消息
139///
140/// 若目录中存在翻译,则使用翻译并用 `args` 替换 `{0}`、`{1}` 等占位符;
141/// 否则返回 `key.default_msg()`。
142pub fn translate(key: MessageKey, args: &[&str]) -> String {
143    let guard = catalog().read().expect("i18n catalog poisoned");
144    if let Some(template) = guard.get(&key) {
145        format_args(template, args)
146    } else {
147        key.default_msg().to_string()
148    }
149}
150
151/// 格式化占位符
152///
153/// 将 `{0}`、`{1}` 等替换为 `args` 中对应索引的字符串。
154/// 越界索引保留原占位符。
155fn format_args(template: &str, args: &[&str]) -> String {
156    let mut result = String::with_capacity(template.len());
157    let mut chars = template.chars().peekable();
158    while let Some(c) = chars.next() {
159        if c == '{' {
160            let mut idx_str = String::new();
161            while let Some(&next) = chars.peek() {
162                if next == '}' {
163                    chars.next();
164                    break;
165                }
166                idx_str.push(next);
167                chars.next();
168            }
169            if let Ok(idx) = idx_str.parse::<usize>() {
170                if let Some(arg) = args.get(idx) {
171                    result.push_str(arg);
172                } else {
173                    result.push('{');
174                    result.push_str(&idx_str);
175                    result.push('}');
176                }
177            } else {
178                result.push('{');
179                result.push_str(&idx_str);
180                result.push('}');
181            }
182        } else {
183            result.push(c);
184        }
185    }
186    result
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn test_default_message() {
195        assert_eq!(MessageKey::ConnectionFailed.default_msg(), "连接失败");
196        assert_eq!(MessageKey::QueryError.default_msg(), "查询错误");
197    }
198
199    #[test]
200    fn test_translate_default() {
201        clear();
202        let msg = translate(MessageKey::ConnectionFailed, &[]);
203        assert_eq!(msg, "连接失败");
204    }
205
206    #[test]
207    fn test_translate_with_catalog() {
208        clear();
209        let mut catalog = MessageCatalog::new();
210        catalog.insert(
211            MessageKey::ConnectionFailed,
212            "Connection failed: {0}".to_string(),
213        );
214        set_catalog(catalog);
215        let msg = translate(MessageKey::ConnectionFailed, &["timeout"]);
216        assert_eq!(msg, "Connection failed: timeout");
217        clear();
218    }
219
220    #[test]
221    fn test_register_single() {
222        clear();
223        register(MessageKey::NotFound, "Not found");
224        let msg = translate(MessageKey::NotFound, &[]);
225        assert_eq!(msg, "Not found");
226        clear();
227    }
228
229    #[test]
230    fn test_format_args_out_of_bounds() {
231        let result = format_args("Hello {0} {1}", &["world"]);
232        assert_eq!(result, "Hello world {1}");
233    }
234
235    #[test]
236    fn test_format_args_no_placeholders() {
237        let result = format_args("Hello world", &[]);
238        assert_eq!(result, "Hello world");
239    }
240
241    #[test]
242    fn test_format_args_invalid_index() {
243        let result = format_args("Hello {abc}", &[]);
244        assert_eq!(result, "Hello {abc}");
245    }
246
247    #[test]
248    fn test_clear() {
249        register(MessageKey::QueryError, "Query error");
250        assert_eq!(translate(MessageKey::QueryError, &[]), "Query error");
251        clear();
252        assert_eq!(translate(MessageKey::QueryError, &[]), "查询错误");
253    }
254}