koprogo_api/domain/
i18n.rs

1use serde::{Deserialize, Serialize};
2
3/// Supported languages
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
5pub enum Language {
6    #[default]
7    NL, // Nederlands (Dutch) - 60% Belgium
8    FR, // Français (French) - 40% Belgium
9    DE, // Deutsch (German) - <1% Belgium
10    EN, // English - International
11}
12
13impl Language {
14    /// Parse language from Accept-Language header or language code
15    pub fn from_code(code: &str) -> Option<Self> {
16        match code.to_lowercase().as_str() {
17            "nl" => Some(Language::NL),
18            "fr" => Some(Language::FR),
19            "de" => Some(Language::DE),
20            "en" => Some(Language::EN),
21            _ => None,
22        }
23    }
24
25    /// Get language code (e.g., "nl", "fr", "de", "en")
26    pub fn code(&self) -> &'static str {
27        match self {
28            Language::NL => "nl",
29            Language::FR => "fr",
30            Language::DE => "de",
31            Language::EN => "en",
32        }
33    }
34}
35
36/// Translation keys for error messages
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum TranslationKey {
39    // Building errors
40    BuildingNameEmpty,
41    TotalUnitsMustBePositive,
42
43    // Expense errors
44    DescriptionEmpty,
45    AmountMustBePositive,
46
47    // Owner errors
48    FirstNameEmpty,
49    LastNameEmpty,
50    InvalidEmailFormat,
51
52    // General errors
53    NotFound,
54    Unauthorized,
55    InternalError,
56}
57
58/// Translation service
59pub struct I18n;
60
61impl I18n {
62    /// Get translated message for a key and language
63    pub fn translate(key: TranslationKey, lang: Language) -> String {
64        use Language::*;
65        use TranslationKey::*;
66
67        match (key, lang) {
68            // Building errors
69            (BuildingNameEmpty, NL) => "Gebouwnaam mag niet leeg zijn".to_string(),
70            (BuildingNameEmpty, FR) => "Le nom du bâtiment ne peut pas être vide".to_string(),
71            (BuildingNameEmpty, DE) => "Gebäudename darf nicht leer sein".to_string(),
72            (BuildingNameEmpty, EN) => "Building name cannot be empty".to_string(),
73
74            (TotalUnitsMustBePositive, NL) => {
75                "Totaal aantal eenheden moet groter zijn dan 0".to_string()
76            }
77            (TotalUnitsMustBePositive, FR) => {
78                "Le nombre total d'unités doit être supérieur à 0".to_string()
79            }
80            (TotalUnitsMustBePositive, DE) => {
81                "Gesamtzahl der Einheiten muss größer als 0 sein".to_string()
82            }
83            (TotalUnitsMustBePositive, EN) => "Total units must be greater than 0".to_string(),
84
85            // Expense errors
86            (DescriptionEmpty, NL) => "Beschrijving mag niet leeg zijn".to_string(),
87            (DescriptionEmpty, FR) => "La description ne peut pas être vide".to_string(),
88            (DescriptionEmpty, DE) => "Beschreibung darf nicht leer sein".to_string(),
89            (DescriptionEmpty, EN) => "Description cannot be empty".to_string(),
90
91            (AmountMustBePositive, NL) => "Bedrag moet groter zijn dan 0".to_string(),
92            (AmountMustBePositive, FR) => "Le montant doit être supérieur à 0".to_string(),
93            (AmountMustBePositive, DE) => "Betrag muss größer als 0 sein".to_string(),
94            (AmountMustBePositive, EN) => "Amount must be greater than 0".to_string(),
95
96            // Owner errors
97            (FirstNameEmpty, NL) => "Voornaam mag niet leeg zijn".to_string(),
98            (FirstNameEmpty, FR) => "Le prénom ne peut pas être vide".to_string(),
99            (FirstNameEmpty, DE) => "Vorname darf nicht leer sein".to_string(),
100            (FirstNameEmpty, EN) => "First name cannot be empty".to_string(),
101
102            (LastNameEmpty, NL) => "Achternaam mag niet leeg zijn".to_string(),
103            (LastNameEmpty, FR) => "Le nom de famille ne peut pas être vide".to_string(),
104            (LastNameEmpty, DE) => "Nachname darf nicht leer sein".to_string(),
105            (LastNameEmpty, EN) => "Last name cannot be empty".to_string(),
106
107            (InvalidEmailFormat, NL) => "Ongeldig e-mailformaat".to_string(),
108            (InvalidEmailFormat, FR) => "Format d'e-mail invalide".to_string(),
109            (InvalidEmailFormat, DE) => "Ungültiges E-Mail-Format".to_string(),
110            (InvalidEmailFormat, EN) => "Invalid email format".to_string(),
111
112            // General errors
113            (NotFound, NL) => "Niet gevonden".to_string(),
114            (NotFound, FR) => "Non trouvé".to_string(),
115            (NotFound, DE) => "Nicht gefunden".to_string(),
116            (NotFound, EN) => "Not found".to_string(),
117
118            (Unauthorized, NL) => "Niet geautoriseerd".to_string(),
119            (Unauthorized, FR) => "Non autorisé".to_string(),
120            (Unauthorized, DE) => "Nicht autorisiert".to_string(),
121            (Unauthorized, EN) => "Unauthorized".to_string(),
122
123            (InternalError, NL) => "Interne serverfout".to_string(),
124            (InternalError, FR) => "Erreur interne du serveur".to_string(),
125            (InternalError, DE) => "Interner Serverfehler".to_string(),
126            (InternalError, EN) => "Internal server error".to_string(),
127        }
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    // ===== Language Code Tests =====
136
137    #[test]
138    fn test_language_from_code_nl() {
139        assert_eq!(Language::from_code("nl"), Some(Language::NL));
140        assert_eq!(Language::from_code("NL"), Some(Language::NL));
141    }
142
143    #[test]
144    fn test_language_from_code_fr() {
145        assert_eq!(Language::from_code("fr"), Some(Language::FR));
146        assert_eq!(Language::from_code("FR"), Some(Language::FR));
147    }
148
149    #[test]
150    fn test_language_from_code_de() {
151        assert_eq!(Language::from_code("de"), Some(Language::DE));
152        assert_eq!(Language::from_code("DE"), Some(Language::DE));
153    }
154
155    #[test]
156    fn test_language_from_code_en() {
157        assert_eq!(Language::from_code("en"), Some(Language::EN));
158        assert_eq!(Language::from_code("EN"), Some(Language::EN));
159    }
160
161    #[test]
162    fn test_language_from_code_invalid() {
163        assert_eq!(Language::from_code("es"), None);
164        assert_eq!(Language::from_code("invalid"), None);
165    }
166
167    #[test]
168    fn test_language_code() {
169        assert_eq!(Language::NL.code(), "nl");
170        assert_eq!(Language::FR.code(), "fr");
171        assert_eq!(Language::DE.code(), "de");
172        assert_eq!(Language::EN.code(), "en");
173    }
174
175    #[test]
176    fn test_language_default_is_dutch() {
177        assert_eq!(Language::default(), Language::NL);
178    }
179
180    // ===== Translation Tests =====
181
182    #[test]
183    fn test_translate_building_name_empty_nl() {
184        let result = I18n::translate(TranslationKey::BuildingNameEmpty, Language::NL);
185        assert_eq!(result, "Gebouwnaam mag niet leeg zijn");
186    }
187
188    #[test]
189    fn test_translate_building_name_empty_fr() {
190        let result = I18n::translate(TranslationKey::BuildingNameEmpty, Language::FR);
191        assert_eq!(result, "Le nom du bâtiment ne peut pas être vide");
192    }
193
194    #[test]
195    fn test_translate_building_name_empty_de() {
196        let result = I18n::translate(TranslationKey::BuildingNameEmpty, Language::DE);
197        assert_eq!(result, "Gebäudename darf nicht leer sein");
198    }
199
200    #[test]
201    fn test_translate_building_name_empty_en() {
202        let result = I18n::translate(TranslationKey::BuildingNameEmpty, Language::EN);
203        assert_eq!(result, "Building name cannot be empty");
204    }
205
206    #[test]
207    fn test_translate_total_units_positive_nl() {
208        let result = I18n::translate(TranslationKey::TotalUnitsMustBePositive, Language::NL);
209        assert_eq!(result, "Totaal aantal eenheden moet groter zijn dan 0");
210    }
211
212    #[test]
213    fn test_translate_total_units_positive_fr() {
214        let result = I18n::translate(TranslationKey::TotalUnitsMustBePositive, Language::FR);
215        assert_eq!(result, "Le nombre total d'unités doit être supérieur à 0");
216    }
217
218    #[test]
219    fn test_translate_description_empty_nl() {
220        let result = I18n::translate(TranslationKey::DescriptionEmpty, Language::NL);
221        assert_eq!(result, "Beschrijving mag niet leeg zijn");
222    }
223
224    #[test]
225    fn test_translate_description_empty_fr() {
226        let result = I18n::translate(TranslationKey::DescriptionEmpty, Language::FR);
227        assert_eq!(result, "La description ne peut pas être vide");
228    }
229
230    #[test]
231    fn test_translate_amount_positive_nl() {
232        let result = I18n::translate(TranslationKey::AmountMustBePositive, Language::NL);
233        assert_eq!(result, "Bedrag moet groter zijn dan 0");
234    }
235
236    #[test]
237    fn test_translate_amount_positive_fr() {
238        let result = I18n::translate(TranslationKey::AmountMustBePositive, Language::FR);
239        assert_eq!(result, "Le montant doit être supérieur à 0");
240    }
241
242    #[test]
243    fn test_translate_first_name_empty_nl() {
244        let result = I18n::translate(TranslationKey::FirstNameEmpty, Language::NL);
245        assert_eq!(result, "Voornaam mag niet leeg zijn");
246    }
247
248    #[test]
249    fn test_translate_first_name_empty_fr() {
250        let result = I18n::translate(TranslationKey::FirstNameEmpty, Language::FR);
251        assert_eq!(result, "Le prénom ne peut pas être vide");
252    }
253
254    #[test]
255    fn test_translate_last_name_empty_de() {
256        let result = I18n::translate(TranslationKey::LastNameEmpty, Language::DE);
257        assert_eq!(result, "Nachname darf nicht leer sein");
258    }
259
260    #[test]
261    fn test_translate_last_name_empty_en() {
262        let result = I18n::translate(TranslationKey::LastNameEmpty, Language::EN);
263        assert_eq!(result, "Last name cannot be empty");
264    }
265
266    #[test]
267    fn test_translate_invalid_email_nl() {
268        let result = I18n::translate(TranslationKey::InvalidEmailFormat, Language::NL);
269        assert_eq!(result, "Ongeldig e-mailformaat");
270    }
271
272    #[test]
273    fn test_translate_invalid_email_fr() {
274        let result = I18n::translate(TranslationKey::InvalidEmailFormat, Language::FR);
275        assert_eq!(result, "Format d'e-mail invalide");
276    }
277
278    #[test]
279    fn test_translate_invalid_email_de() {
280        let result = I18n::translate(TranslationKey::InvalidEmailFormat, Language::DE);
281        assert_eq!(result, "Ungültiges E-Mail-Format");
282    }
283
284    #[test]
285    fn test_translate_invalid_email_en() {
286        let result = I18n::translate(TranslationKey::InvalidEmailFormat, Language::EN);
287        assert_eq!(result, "Invalid email format");
288    }
289}