countries_iso3166/bcp47/
single_lang_parser.rs1use 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 if language.is_none() && line.starts_with('#') {
30 language = Some(line.trim_start_matches('#').trim().to_string());
31 continue;
32 }
33
34 if in_multiline {
36 multiline_value.push('\n');
37 multiline_value.push_str(line);
38
39 if line.ends_with('"') {
40 multiline_value.pop(); 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 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); if value.ends_with('"') {
59 value.pop(); translations.insert(key, value);
61 } else {
62 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}