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