1use crate::{CheckResult, ObjectLocation};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
4pub enum Language {
5 English,
6 Chinese,
7 TraditionalChinese,
8 Japanese,
9 Korean,
10 German,
11 French,
12 Spanish,
13 Portuguese,
14 Arabic,
15 Thai,
16 Indonesian,
17 Filipino,
18 Ukrainian,
19 Vietnamese,
20}
21
22impl Default for Language {
23 fn default() -> Self {
24 Self::English
25 }
26}
27
28impl Language {
29 pub const ALL: [Language; 15] = [
30 Language::English,
31 Language::Chinese,
32 Language::TraditionalChinese,
33 Language::Japanese,
34 Language::Korean,
35 Language::German,
36 Language::French,
37 Language::Spanish,
38 Language::Portuguese,
39 Language::Arabic,
40 Language::Thai,
41 Language::Indonesian,
42 Language::Filipino,
43 Language::Ukrainian,
44 Language::Vietnamese,
45 ];
46
47 pub fn code(self) -> &'static str {
48 match self {
49 Self::English => "en",
50 Self::Chinese => "zh-CN",
51 Self::TraditionalChinese => "zh-TW",
52 Self::Japanese => "ja",
53 Self::Korean => "ko",
54 Self::German => "de",
55 Self::French => "fr",
56 Self::Spanish => "es",
57 Self::Portuguese => "pt",
58 Self::Arabic => "ar",
59 Self::Thai => "th",
60 Self::Indonesian => "id",
61 Self::Filipino => "fil",
62 Self::Ukrainian => "uk",
63 Self::Vietnamese => "vi",
64 }
65 }
66
67 pub fn from_code(code: &str) -> Option<Self> {
68 let normalized = code.trim().replace('_', "-").to_ascii_lowercase();
69 match normalized.as_str() {
70 "zh" | "zh-cn" | "zh-hans" | "zh-sg" | "cn" => Some(Self::Chinese),
71 "zh-tw" | "zh-hant" | "zh-hk" | "zh-mo" | "tw" => Some(Self::TraditionalChinese),
72 "tl" => Some(Self::Filipino),
73 "zh-latn" | "" => None,
74 _ => match normalized.split('-').next()? {
75 "en" => Some(Self::English),
76 "ja" => Some(Self::Japanese),
77 "ko" => Some(Self::Korean),
78 "de" => Some(Self::German),
79 "fr" => Some(Self::French),
80 "es" => Some(Self::Spanish),
81 "pt" => Some(Self::Portuguese),
82 "ar" => Some(Self::Arabic),
83 "th" => Some(Self::Thai),
84 "id" => Some(Self::Indonesian),
85 "fil" => Some(Self::Filipino),
86 "uk" => Some(Self::Ukrainian),
87 "vi" => Some(Self::Vietnamese),
88 _ => None,
89 },
90 }
91 }
92}
93
94pub type Locale = Language;
95
96pub trait MessageTranslator: Send + Sync {
97 fn language(&self) -> Language;
98 fn translate_check_result(&self, result: &CheckResult) -> String;
99}
100
101#[derive(Debug, Clone, Copy)]
102pub struct BuiltinTranslator {
103 language: Language,
104}
105
106impl BuiltinTranslator {
107 pub fn new(language: Language) -> Self {
108 Self { language }
109 }
110}
111
112impl MessageTranslator for BuiltinTranslator {
113 fn language(&self) -> Language {
114 self.language
115 }
116
117 fn translate_check_result(&self, result: &CheckResult) -> String {
118 translate_check_result(self.language, result)
119 }
120}
121
122pub fn translate_check_result(language: Language, result: &CheckResult) -> String {
123 crate::I18nCatalog::builtin().translate_check_result(language, result)
124}
125
126pub fn translate_location(_language: Language, location: &ObjectLocation) -> String {
127 title_case_path(&location.to_string())
128}
129
130fn title_case_path(path: &str) -> String {
131 path.split('.')
132 .map(|part| {
133 part.split_once('[')
134 .map(|(name, index)| format!("{}[{}", title_case_identifier(name), index))
135 .unwrap_or_else(|| title_case_identifier(part))
136 })
137 .collect::<Vec<_>>()
138 .join(".")
139}
140
141fn title_case_identifier(value: &str) -> String {
142 let mut output = String::new();
143 for (index, ch) in value.chars().enumerate() {
144 if index > 0 && ch.is_uppercase() {
145 output.push(' ');
146 }
147 match index {
148 0 => output.extend(ch.to_uppercase()),
149 _ => output.extend(ch.to_lowercase()),
150 }
151 }
152 output
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn test_language_code_aliases_and_canonical_codes() {
161 assert_eq!(Language::English.code(), "en");
163 assert_eq!(Language::Chinese.code(), "zh-CN");
164 assert_eq!(Language::TraditionalChinese.code(), "zh-TW");
165 assert_eq!(Language::Filipino.code(), "fil");
166
167 assert_eq!(Language::from_code("en"), Some(Language::English));
169 assert_eq!(Language::from_code("zh-CN"), Some(Language::Chinese));
170
171 assert_eq!(Language::from_code("en-US"), Some(Language::English));
173 assert_eq!(Language::from_code("en-GB"), Some(Language::English));
174 assert_eq!(Language::from_code("zh"), Some(Language::Chinese));
175 assert_eq!(Language::from_code("cn"), Some(Language::Chinese));
176 assert_eq!(
177 Language::from_code("zh-HK"),
178 Some(Language::TraditionalChinese)
179 );
180 assert_eq!(
181 Language::from_code("tw"),
182 Some(Language::TraditionalChinese)
183 );
184 assert_eq!(Language::from_code("tl"), Some(Language::Filipino));
185 assert_eq!(Language::from_code("pt-BR"), Some(Language::Portuguese));
186 assert_eq!(Language::from_code("EN_us"), Some(Language::English));
187 assert_eq!(Language::from_code("zh-Hans"), Some(Language::Chinese));
188 assert_eq!(
189 Language::from_code("zh-Hant"),
190 Some(Language::TraditionalChinese)
191 );
192 assert_eq!(Language::from_code("es-MX"), Some(Language::Spanish));
193
194 assert_eq!(Language::from_code("invalid-code"), None);
196 assert_eq!(Language::from_code("zh-Latn"), None);
197 }
198
199 #[test]
200 fn shared_locale_fixture_matches_rust_normalization() {
201 let cases: serde_json::Value =
202 serde_json::from_str(include_str!("locale-cases-v1.json")).unwrap();
203 let canonical = cases["canonical"].as_array().unwrap();
204 assert_eq!(canonical.len(), Language::ALL.len());
205 for code in canonical {
206 let code = code.as_str().unwrap();
207 assert_eq!(Language::from_code(code).unwrap().code(), code);
208 }
209 for (alias, expected) in cases["aliases"].as_object().unwrap() {
210 assert_eq!(
211 Language::from_code(alias).map(Language::code),
212 expected.as_str(),
213 "alias {alias}"
214 );
215 }
216 for unsupported in cases["unsupported"].as_array().unwrap() {
217 assert_eq!(Language::from_code(unsupported.as_str().unwrap()), None);
218 }
219 }
220}