Skip to main content

countries_iso3166/bcp47/
single_lang_parser.rs

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