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