sz_rust_infra_facade/validate/message.rs
1//! 错误消息国际化 — 对齐 PHP `think\Lang` 和 `think\Validate::parseErrorMsg`
2//!
3//! 本模块实现错误消息的国际化翻译机制,对齐 PHP
4//! `think\Validate::parseErrorMsg`(第 1596-1633 行)的 Lang 翻译行为。
5//!
6//! ## PHP 对齐
7//!
8//! ### 核心接口映射
9//!
10//! | PHP 方法 | Rust 接口 | 说明 |
11//! |---------|-----------|------|
12//! | `Lang::has($name)` | [`Lang::has`] | 判断语言定义是否存在(不区分大小写) |
13//! | `Lang::get($name)` | [`Lang::get`] | 获取语言定义(找不到返回 name 本身) |
14//! | `Validate::setLang($lang)` | `Validate::set_lang` | 注入 Lang 实例 |
15//! | `parseErrorMsg` 第 1598-1602 行 | [`translate_msg`] | 消息翻译(`{%var}` + `has` 检查) |
16//!
17//! ### PHP `parseErrorMsg` 翻译逻辑(R5-7)
18//!
19//! 对齐 PHP `Validate.php` 第 1598-1602 行:
20//!
21//! ```php
22//! if (0 === strpos($msg, '{%')) {
23//! $msg = $this->lang->get(substr($msg, 2, -1));
24//! } elseif ($this->lang->has($msg)) {
25//! $msg = $this->lang->get($msg);
26//! }
27//! ```
28//!
29//! 优先级:
30//! 1. 如果 msg 以 `{%` 开头且以 `}` 结尾,提取内部 key 调用 `lang->get`
31//! 2. 否则如果 `lang->has(msg)` 为真,调用 `lang->get(msg)`
32//! 3. 否则返回 msg 原值
33//!
34//! ### PHP `Lang::has` / `Lang::get` 行为
35//!
36//! 对齐 PHP `Lang.php`:
37//!
38//! - **不区分大小写**:`strtolower($name)`(第 231/234 行)
39//! - **找不到时返回 name 本身**:`?? $name`(第 263 行)
40//! - **支持语言分组**:`allow_group` 配置(本实现不涉及,简化版)
41//!
42//! ## 架构说明
43//!
44//! 本模块提供 [`Lang`] trait 作为接口契约,[`SimpleLang`] 作为默认实现。
45//! [`Validate`]`::set_lang` 接受 `Arc<dyn Lang>` 注入实例,`Option::None`
46//! 时跳过翻译(对齐 PHP 未注入 Lang 时的行为)。
47//!
48//! [`Validate`]: crate::validate::Validate
49
50use std::sync::Arc;
51
52use indexmap::IndexMap;
53
54// ============================================================================
55// Lang trait — 对齐 PHP `think\Lang`
56// ============================================================================
57
58/// 多语言接口 — 对齐 PHP `think\Lang`
59///
60/// 对齐 PHP `Lang.php` 第 19-289 行的核心方法。
61///
62/// ## PHP 对齐
63///
64/// - `has(string $name): bool`(第 225-235 行):不区分大小写
65/// - `get(string $name = null, array $vars = []): mixed`(第 245-264 行):
66/// - 不区分大小写
67/// - 找不到时返回 name 本身
68///
69/// ## Send + Sync 约束
70///
71/// 要求 `Send + Sync` 以便作为 `Arc<dyn Lang>` 在 `Validate` 中存储
72/// 并跨线程共享。
73pub trait Lang: Send + Sync {
74 /// 判断是否存在语言定义(对齐 PHP `Lang::has`)
75 ///
76 /// 对齐 PHP `Lang.php` 第 225-235 行
77 ///
78 /// ## PHP 行为
79 ///
80 /// - 键不区分大小写(`strtolower($name)`)
81 /// - 不支持分组(本简化版)
82 fn has(&self, name: &str) -> bool;
83
84 /// 获取语言定义(对齐 PHP `Lang::get`)
85 ///
86 /// 对齐 PHP `Lang.php` 第 245-264 行
87 ///
88 /// ## PHP 行为
89 ///
90 /// - 键不区分大小写
91 /// - 找不到时返回 name 本身(对齐第 263 行 `?? $name`)
92 fn get(&self, name: &str) -> String;
93}
94
95// ============================================================================
96// SimpleLang — 基础实现
97// ============================================================================
98
99/// 简单多语言实现 — 基础键值对存储
100///
101/// 对齐 PHP `think\Lang` 的最小可用实现,不依赖文件加载、cookie、header
102/// 检测等。适用于单元测试和简单场景。
103///
104/// ## PHP 对齐
105///
106/// - 键不区分大小写(对齐 PHP `strtolower($name)`)
107/// - 找不到时返回 name 本身(对齐 PHP `?? $name`)
108///
109/// ## 用法
110///
111/// ```ignore
112/// use sz_rust_core::validate::message::{Lang, SimpleLang};
113///
114/// let lang = SimpleLang::new()
115/// .set("not conform to the rules", "不符合规则")
116/// .set("require", "必须填写");
117/// assert!(lang.has("require"));
118/// assert_eq!(lang.get("require"), "必须填写");
119/// assert_eq!(lang.get("NOT FOUND"), "NOT FOUND"); // 找不到返回原值
120/// ```
121#[derive(Debug, Clone, Default)]
122pub struct SimpleLang {
123 /// 语言包(键已 lowercase,对齐 PHP `strtolower`)
124 pack: IndexMap<String, String>,
125}
126
127impl SimpleLang {
128 /// 创建空的语言包
129 pub fn new() -> Self {
130 Self::default()
131 }
132
133 /// 添加翻译条目(对齐 PHP `$this->lang[$range][strtolower($name)] = $value`)
134 pub fn set(mut self, name: &str, value: &str) -> Self {
135 self.pack.insert(name.to_lowercase(), value.to_string());
136 self
137 }
138
139 /// 批量添加翻译条目(对齐 PHP `Lang::load` 合并行为)
140 pub fn extend(mut self, entries: IndexMap<String, String>) -> Self {
141 for (k, v) in entries {
142 self.pack.insert(k.to_lowercase(), v);
143 }
144 self
145 }
146}
147
148impl Lang for SimpleLang {
149 fn has(&self, name: &str) -> bool {
150 self.pack.contains_key(&name.to_lowercase())
151 }
152
153 fn get(&self, name: &str) -> String {
154 self.pack
155 .get(&name.to_lowercase())
156 .cloned()
157 .unwrap_or_else(|| name.to_string())
158 }
159}
160
161// ============================================================================
162// 翻译函数 — 对齐 PHP `parseErrorMsg` 第 1598-1602 行
163// ============================================================================
164
165/// 翻译消息 — 对齐 PHP `Validate::parseErrorMsg` 第 1598-1602 行
166///
167/// ## PHP 行为(R5-7)
168///
169/// ```php
170/// if (0 === strpos($msg, '{%')) {
171/// $msg = $this->lang->get(substr($msg, 2, -1));
172/// } elseif ($this->lang->has($msg)) {
173/// $msg = $this->lang->get($msg);
174/// }
175/// ```
176///
177/// 优先级:
178/// 1. `{%key}` 语法:提取 key 调用 `lang->get(key)`
179/// 2. `lang->has(msg)`:调用 `lang->get(msg)`
180/// 3. 否则返回 msg 原值
181///
182/// ## 参数
183///
184/// - `msg`:原始消息
185/// - `lang`:可选的多语言实例(`None` 时跳过翻译)
186///
187/// ## 返回
188///
189/// 翻译后的消息(如果适用),否则返回原消息
190pub fn translate_msg(msg: &str, lang: Option<&Arc<dyn Lang>>) -> String {
191 let Some(lang) = lang else {
192 return msg.to_string();
193 };
194
195 // 对齐 PHP 第 1598-1599 行:{%var} 语法
196 // substr($msg, 2, -1) — 去掉开头 "{%" 和结尾 "}"
197 if let Some(stripped) = msg.strip_prefix("{%").and_then(|s| s.strip_suffix('}')) {
198 if !stripped.is_empty() {
199 return lang.get(stripped);
200 }
201 }
202
203 // 对齐 PHP 第 1600-1601 行:lang->has 检查
204 if lang.has(msg) {
205 return lang.get(msg);
206 }
207
208 msg.to_string()
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 // ========================================================================
216 // 组 1:SimpleLang 基础测试
217 // ========================================================================
218
219 #[test]
220 fn test_simple_lang_new_empty() {
221 let lang = SimpleLang::new();
222 assert!(!lang.has("any"));
223 assert_eq!(lang.get("any"), "any");
224 }
225
226 #[test]
227 fn test_simple_lang_set_and_get() {
228 let lang = SimpleLang::new().set("require", "必须填写");
229 assert!(lang.has("require"));
230 assert_eq!(lang.get("require"), "必须填写");
231 }
232
233 #[test]
234 fn test_simple_lang_case_insensitive() {
235 // 对齐 PHP strtolower 行为
236 let lang = SimpleLang::new().set("Require", "必须填写");
237 assert!(lang.has("require"));
238 assert!(lang.has("REQUIRE"));
239 assert!(lang.has("Require"));
240 assert_eq!(lang.get("REQUIRE"), "必须填写");
241 }
242
243 #[test]
244 fn test_simple_lang_not_found_returns_name() {
245 // 对齐 PHP `?? $name` 行为
246 let lang = SimpleLang::new().set("require", "必须填写");
247 assert_eq!(lang.get("not_found"), "not_found");
248 }
249
250 #[test]
251 fn test_simple_lang_chained_set() {
252 let lang = SimpleLang::new()
253 .set("require", "必须填写")
254 .set("email", "邮箱格式错误");
255 assert_eq!(lang.get("require"), "必须填写");
256 assert_eq!(lang.get("email"), "邮箱格式错误");
257 }
258
259 #[test]
260 fn test_simple_lang_extend() {
261 let mut entries = IndexMap::new();
262 entries.insert("require".to_string(), "必须".to_string());
263 entries.insert("email".to_string(), "邮箱错误".to_string());
264 let lang = SimpleLang::new().extend(entries);
265 assert_eq!(lang.get("require"), "必须");
266 assert_eq!(lang.get("email"), "邮箱错误");
267 }
268
269 #[test]
270 fn test_simple_lang_override() {
271 // 后设置的覆盖先设置的(对齐 PHP `+` 运算符行为)
272 let lang = SimpleLang::new()
273 .set("require", "旧值")
274 .set("require", "新值");
275 assert_eq!(lang.get("require"), "新值");
276 }
277
278 // ========================================================================
279 // 组 2:translate_msg 测试(R5-7)
280 // ========================================================================
281
282 #[test]
283 fn test_translate_msg_none_lang_returns_original() {
284 // 无 Lang 实例时返回原消息
285 let result = translate_msg("hello world", None);
286 assert_eq!(result, "hello world");
287 }
288
289 #[test]
290 fn test_translate_msg_percent_var_syntax() {
291 // 对齐 PHP `{%var}` 语法
292 let lang: Arc<dyn Lang> = Arc::new(SimpleLang::new().set("hello", "你好"));
293 let result = translate_msg("{%hello}", Some(&lang));
294 assert_eq!(result, "你好");
295 }
296
297 #[test]
298 fn test_translate_msg_percent_var_not_found_returns_key() {
299 // 对齐 PHP `Lang::get` 找不到返回 name 行为
300 let lang: Arc<dyn Lang> = Arc::new(SimpleLang::new());
301 let result = translate_msg("{%not_found}", Some(&lang));
302 assert_eq!(result, "not_found");
303 }
304
305 #[test]
306 fn test_translate_msg_lang_has_check() {
307 // 对齐 PHP `lang->has($msg)` 检查
308 let lang: Arc<dyn Lang> = Arc::new(SimpleLang::new().set("require", "必须填写"));
309 let result = translate_msg("require", Some(&lang));
310 assert_eq!(result, "必须填写");
311 }
312
313 #[test]
314 fn test_translate_msg_no_match_returns_original() {
315 // 既不是 {%var} 也不在 lang 中
316 let lang: Arc<dyn Lang> = Arc::new(SimpleLang::new().set("require", "必须填写"));
317 let result = translate_msg("custom message", Some(&lang));
318 assert_eq!(result, "custom message");
319 }
320
321 #[test]
322 fn test_translate_msg_percent_var_priority_over_has() {
323 // {%var} 优先于 has 检查
324 let lang: Arc<dyn Lang> = Arc::new(
325 SimpleLang::new()
326 .set("require", "直接翻译")
327 .set("custom_key", "key翻译"),
328 );
329 // {%custom_key} 应该返回 "key翻译"
330 let result = translate_msg("{%custom_key}", Some(&lang));
331 assert_eq!(result, "key翻译");
332 }
333
334 #[test]
335 fn test_translate_msg_empty_percent_var() {
336 // {%} 空键情况 — 不应该走 {%var} 分支
337 let lang: Arc<dyn Lang> = Arc::new(SimpleLang::new());
338 let result = translate_msg("{}", Some(&lang));
339 // 不以 {% 开头,走 has 检查 → 不存在 → 返回原值
340 assert_eq!(result, "{}");
341 }
342
343 #[test]
344 fn test_translate_msg_only_prefix() {
345 // "{%" 不完整,不应该走 {%var} 分支
346 let lang: Arc<dyn Lang> = Arc::new(SimpleLang::new());
347 let result = translate_msg("{%", Some(&lang));
348 // strip_suffix('}') 失败,走 has 检查 → 不存在 → 返回原值
349 assert_eq!(result, "{%");
350 }
351
352 #[test]
353 fn test_translate_msg_percent_var_with_special_chars() {
354 // {%var} 中包含特殊字符
355 let lang: Arc<dyn Lang> = Arc::new(SimpleLang::new().set("foo.bar", "点号键"));
356 let result = translate_msg("{%foo.bar}", Some(&lang));
357 assert_eq!(result, "点号键");
358 }
359
360 // ========================================================================
361 // 组 3:PHP 行为对齐测试
362 // ========================================================================
363
364 #[test]
365 fn test_php_behavior_lang_case_insensitive() {
366 // 对齐 PHP Lang::has/get 的 strtolower 行为
367 let lang: Arc<dyn Lang> = Arc::new(SimpleLang::new().set("Hello", "你好"));
368 assert!(lang.has("hello"));
369 assert!(lang.has("HELLO"));
370 assert_eq!(lang.get("hello"), "你好");
371 assert_eq!(lang.get("HELLO"), "你好");
372 }
373
374 #[test]
375 fn test_php_behavior_lang_get_returns_name_when_not_found() {
376 // 对齐 PHP Lang::get 第 263 行 `?? $name`
377 let lang: Arc<dyn Lang> = Arc::new(SimpleLang::new());
378 assert_eq!(lang.get("nonexistent"), "nonexistent");
379 assert_eq!(lang.get("Some Key"), "Some Key");
380 }
381
382 #[test]
383 fn test_php_behavior_translate_percent_var_extracts_key() {
384 // 对齐 PHP substr($msg, 2, -1) 提取 key
385 let lang: Arc<dyn Lang> = Arc::new(SimpleLang::new().set("my_key", "翻译值"));
386 let result = translate_msg("{%my_key}", Some(&lang));
387 assert_eq!(result, "翻译值");
388 }
389
390 #[test]
391 fn test_php_behavior_translate_has_check_falls_back_to_original() {
392 // 对齐 PHP elseif 分支:has 为 false 时返回原值
393 let lang: Arc<dyn Lang> = Arc::new(SimpleLang::new());
394 let result = translate_msg("plain message", Some(&lang));
395 assert_eq!(result, "plain message");
396 }
397
398 #[test]
399 fn test_php_behavior_translate_priority_percent_over_has() {
400 // 对齐 PHP if 优先于 elseif
401 // 当 msg 是 {%var} 形式时,即使 lang->has(msg) 为真,也走 {%var} 分支
402 let lang: Arc<dyn Lang> = Arc::new(
403 SimpleLang::new()
404 .set("{%key}", "整体键值") // 这种情况理论上不会出现
405 .set("key", "提取键值"),
406 );
407 let result = translate_msg("{%key}", Some(&lang));
408 // {%key} 应该提取 "key" 然后翻译,而不是把 "{%key}" 作为整体键
409 assert_eq!(result, "提取键值");
410 }
411}