Skip to main content

teaql_runtime/
language.rs

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        // Test canonical codes
157        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        // Test from_code with canonical codes
163        assert_eq!(Language::from_code("en"), Some(Language::English));
164        assert_eq!(Language::from_code("zh-CN"), Some(Language::Chinese));
165
166        // Test from_code with aliases
167        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        // Test invalid code
190        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}