Skip to main content

countries_iso3166/bcp47/
single_lang_parser.rs

1use std::collections::HashMap;
2
3use crate::{BCP47LanguageInfo, CountriesIso31661Error, CountriesIso31661Result};
4
5#[derive(Debug, Clone, PartialEq, Eq, Default)]
6#[cfg_attr(feature = "bitcode", derive(bitcode::Encode, bitcode::Decode))]
7#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
8pub struct SingleLanguageTranslationMap {
9    pub bcp47_code: String,
10    pub translations: HashMap<String, String>,
11}
12
13impl SingleLanguageTranslationMap {
14    pub fn parse(source_path: &str, input: &str) -> CountriesIso31661Result<Self> {
15        let lines = input.lines();
16        let mut language = None;
17        let mut translations = HashMap::new();
18
19        let mut current_key: Option<String> = None;
20        let mut multiline_value = String::new();
21        let mut in_multiline = false;
22
23        for line in lines {
24            let line = line.trim();
25
26            if line.is_empty() {
27                continue;
28            }
29
30            // First non-empty line starting with # = language
31            if language.is_none() && line.starts_with('#') {
32                language = Some(line.trim_start_matches('#').trim().to_string());
33                continue;
34            }
35
36            // Multiline continuation
37            if in_multiline {
38                multiline_value.push('\n');
39                multiline_value.push_str(line);
40
41                if line.ends_with('"') {
42                    multiline_value.pop(); // remove trailing quote
43                    if let Some(key) = current_key.take() {
44                        translations.insert(key, multiline_value.clone());
45                    }
46                    multiline_value.clear();
47                    in_multiline = false;
48                }
49
50                continue;
51            }
52
53            // Parse key = value
54            if let Some((key, value)) = line.split_once('=') {
55                let key = key.trim().to_string();
56                let mut value = value.trim().to_string();
57
58                if value.starts_with('"') {
59                    value.remove(0); // remove leading quote
60                    if value.ends_with('"') {
61                        value.pop(); // remove trailing quote
62                        translations.insert(key, value);
63                    } else {
64                        // multiline value starts
65                        in_multiline = true;
66                        current_key = Some(key);
67                        multiline_value = value;
68                    }
69                } else {
70                    translations.insert(key, value);
71                }
72            } else {
73                return Err(CountriesIso31661Error::InvalidLanguageEntryParsed {
74                    source_path: source_path.to_string(),
75                    line: line.to_string(),
76                });
77            }
78        }
79
80        let bcp47_code = language.ok_or(CountriesIso31661Error::LanguageBcp47CodeNotFound(
81            source_path.to_string(),
82        ))?;
83
84        let parsed_code: BCP47LanguageInfo = bcp47_code.as_str().into();
85
86        if parsed_code == BCP47LanguageInfo::UnsupportedLanguage {
87            return Err(CountriesIso31661Error::UnsupportedBcp47Code {
88                source_path: source_path.to_string(),
89                invalid_lang: bcp47_code,
90            });
91        }
92
93        Ok(Self {
94            bcp47_code,
95            translations,
96        })
97    }
98
99    pub fn get_translation(&self, key: &str) -> Option<&String> {
100        self.translations.get(key)
101    }
102
103    pub fn bcp47_code(&self) -> &str {
104        self.bcp47_code.as_str()
105    }
106
107    pub fn translations(&self) -> &HashMap<String, String> {
108        &self.translations
109    }
110
111    pub fn translations_owned(&self) -> Vec<(String, String)> {
112        self.translations
113            .iter()
114            .map(|(key, value)| (key.clone(), value.clone()))
115            .collect()
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use crate::SingleLanguageTranslationMap;
122
123    #[test]
124    fn valid_lang() {
125        let source_contents = include_str!("../../example_data/test-single-lang.bcp47");
126        let source_path = "../../example_data/test-single-lang.bcp47";
127
128        let parse = SingleLanguageTranslationMap::parse(source_path, source_contents);
129
130        assert!(parse.is_ok());
131    }
132
133    #[test]
134    fn invalid_lang() {
135        const LANG: &str = r#"""
136        hello_world = hello world
137        lorem = "Lorem ipsum dolor sit amet consectetur adipisicing elit. 
138        Fuga impedit porro possimus quo obcaecati molestias perferendis, consectetur iure natus.
139        At ipsa laudantium iusto illo fuga tempora facilis. Vero, tempora libero."
140        """#;
141
142        let parse = SingleLanguageTranslationMap::parse("static str", LANG);
143
144        assert!(parse.is_err());
145    }
146}