//! 主流语言注册表:把语言代码规范化为模型可用的名称与书写要求。 //! //! 放在协议 crate 中,使任务生产方(git / pulls)与消费方(agent / aihost) //! 共用同一份规范化逻辑而不产生 crate 依赖环。 //! //! 平台不限制输出语言:`[ai].output_language` 只是缺省值,每个任务可携带 //! 自己的语言(通常来自触发者的 `users.language` / `users.locale`)。 //! 未知但语法合法的 BCP-47 代码会原样透传给模型,不报错。 /// `(代码, 英文名, 本语名)`。 /// /// 代码统一小写、`-` 分隔;`normalize` 会把地区变体折叠到这些条目上。 const SUPPORTED: &[(&str, &str, &str)] = &[ ("zh-cn", "Simplified Chinese", "简体中文"), ("zh-tw", "Traditional Chinese", "繁體中文"), ("en", "English", "English"), ("ja", "Japanese", "日本語"), ("ko", "Korean", "한국어"), ("fr", "French", "Français"), ("de", "German", "Deutsch"), ("es", "Spanish", "Español"), ("pt", "Portuguese", "Português"), ("it", "Italian", "Italiano"), ("ru", "Russian", "Русский"), ("uk", "Ukrainian", "Українська"), ("nl", "Dutch", "Nederlands"), ("pl", "Polish", "Polski"), ("cs", "Czech", "Čeština"), ("sv", "Swedish", "Svenska"), ("no", "Norwegian", "Norsk"), ("da", "Danish", "Dansk"), ("fi", "Finnish", "Suomi"), ("tr", "Turkish", "Türkçe"), ("ar", "Arabic", "العربية"), ("he", "Hebrew", "עברית"), ("fa", "Persian", "فارسی"), ("hi", "Hindi", "हिन्दी"), ("bn", "Bengali", "বাংলা"), ("ta", "Tamil", "தமிழ்"), ("th", "Thai", "ไทย"), ("vi", "Vietnamese", "Tiếng Việt"), ("id", "Indonesian", "Bahasa Indonesia"), ("ms", "Malay", "Bahasa Melayu"), ("el", "Greek", "Ελληνικά"), ("ro", "Romanian", "Română"), ("hu", "Hungarian", "Magyar"), ]; /// 特殊值:不指定语言,由模型沿用被评审内容的语言。 pub const AUTO: &str = "auto"; /// 把任意用户输入规范化为注册表里的代码。 /// /// 处理常见写法:`zh_Hans`、`zh-Hans-CN`、`en-US`、`EN`、`pt_BR`。 /// 命中不了时返回小写规范化结果(仍可能是合法 BCP-47)。 pub fn normalize(code: &str) -> String { let lower = code.trim().to_ascii_lowercase().replace('_', "-"); if lower.is_empty() { return String::new(); } if lower == AUTO || lower == "und" || lower == "system" { return AUTO.to_string(); } // 先精确命中,再按「主语言 + 脚本」逐级回退。 if lookup(&lower).is_some() { return lower; } let mut parts: Vec<&str> = lower.split('-').collect(); // zh-Hans-CN / zh-Hant-TW → zh-cn / zh-tw if parts.first() == Some(&"zh") { let script = parts.get(1).copied().unwrap_or(""); if script.starts_with("hant") || script == "tw" || script == "hk" || script == "mo" { return "zh-tw".to_string(); } if script.starts_with("hans") || script == "cn" || script == "sg" || script == "my" { return "zh-cn".to_string(); } // 仅 "zh":简体为缺省(与 config 默认一致)。 return "zh-cn".to_string(); } // 去掉地区段(en-US → en);保留两段以上的脚本写法(sr-Latn)。 if parts.len() > 1 { parts.truncate(1); let primary = parts.join("-"); if lookup(&primary).is_some() { return primary; } } lower } /// 注册表查询(`code` 需已规范化)。 fn lookup(code: &str) -> Option<(&'static str, &'static str)> { SUPPORTED .iter() .find(|(c, _, _)| *c == code) .map(|(_, en, native)| (*en, *native)) } /// 面向提示词的名称:`简体中文 (Simplified Chinese)`;未知代码返回原样。 pub fn display_name(code: &str) -> String { let normalized = normalize(code); if normalized.is_empty() || normalized == AUTO { return String::new(); } match lookup(&normalized) { Some((en, native)) => format!("{native} ({en})"), None => normalized, } } /// 该代码是否是「沿用内容语言」模式。 pub fn is_auto(code: &str) -> bool { normalize(code) == AUTO } /// 已知语言的代码清单(供 UI 下拉与校验)。 pub fn supported_codes() -> Vec<&'static str> { let mut codes: Vec<&'static str> = SUPPORTED.iter().map(|(c, _, _)| *c).collect(); codes.push(AUTO); codes } /// 校验:空字符串非法;其余一律接受(未知代码交模型处理)。 pub fn validate(code: &str) -> Result<(), String> { if code.trim().is_empty() { return Err("language code must not be empty".to_string()); } let normalized = normalize(code); if normalized.is_empty() { return Err("language code must not be empty".to_string()); } Ok(()) }