1use crate::error::{Result, SearchError};
2use serde_json::Value;
3use std::fs;
4use std::path::{Path, PathBuf};
5
6use super::translation::TranslationEntry;
7
8pub struct JsonParser;
10
11impl JsonParser {
12 pub fn parse_file(path: &Path) -> Result<Vec<TranslationEntry>> {
13 Self::parse_file_with_query(path, None)
14 }
15
16 pub fn parse_file_with_query(
20 path: &Path,
21 query: Option<&str>,
22 ) -> Result<Vec<TranslationEntry>> {
23 let content = fs::read_to_string(path).map_err(|e| {
24 SearchError::json_parse_error(path, format!("Failed to read file: {}", e))
25 })?;
26
27 let cleaned_content = Self::strip_json_comments(&content);
29
30 let root: Value = serde_json::from_str(&cleaned_content).map_err(|e| {
32 SearchError::json_parse_error(path, format!("Invalid JSON syntax: {}", e))
33 })?;
34
35 let mut entries = Vec::new();
36 Self::flatten_json(&root, String::new(), path, &mut entries);
37
38 if let Some(q) = query {
40 let q_lower = q.to_lowercase();
41 entries.retain(|e| e.value.to_lowercase().contains(&q_lower));
42 }
43
44 Ok(entries)
45 }
46
47 fn strip_json_comments(content: &str) -> String {
50 let mut result = String::with_capacity(content.len());
51 let mut chars = content.chars().peekable();
52 let mut in_string = false;
53 let mut escape_next = false;
54
55 while let Some(ch) = chars.next() {
56 if escape_next {
57 result.push(ch);
58 escape_next = false;
59 continue;
60 }
61
62 if ch == '\\' && in_string {
63 result.push(ch);
64 escape_next = true;
65 continue;
66 }
67
68 if ch == '"' {
69 in_string = !in_string;
70 result.push(ch);
71 continue;
72 }
73
74 if !in_string && ch == '/' {
75 if let Some(&next_ch) = chars.peek() {
76 if next_ch == '/' {
77 chars.next(); for c in chars.by_ref() {
80 if c == '\n' {
81 result.push('\n'); break;
83 }
84 }
85 continue;
86 } else if next_ch == '*' {
87 chars.next(); let mut prev = ' ';
90 for c in chars.by_ref() {
91 if prev == '*' && c == '/' {
92 break;
93 }
94 if c == '\n' {
95 result.push('\n'); }
97 prev = c;
98 }
99 continue;
100 }
101 }
102 }
103
104 result.push(ch);
105 }
106
107 result
108 }
109
110 fn flatten_json(
111 value: &Value,
112 prefix: String,
113 file_path: &Path,
114 entries: &mut Vec<TranslationEntry>,
115 ) {
116 match value {
117 Value::Object(map) => {
118 for (key, val) in map {
119 let new_prefix = if prefix.is_empty() {
120 key.clone()
121 } else {
122 format!("{}.{}", prefix, key)
123 };
124
125 Self::flatten_json(val, new_prefix, file_path, entries);
126 }
127 }
128 Value::String(s) => {
129 entries.push(TranslationEntry {
130 key: prefix,
131 value: s.clone(),
132 line: 0, file: PathBuf::from(file_path),
134 });
135 }
136 Value::Number(n) => {
137 entries.push(TranslationEntry {
138 key: prefix,
139 value: n.to_string(),
140 line: 0,
141 file: PathBuf::from(file_path),
142 });
143 }
144 Value::Bool(b) => {
145 entries.push(TranslationEntry {
146 key: prefix,
147 value: b.to_string(),
148 line: 0,
149 file: PathBuf::from(file_path),
150 });
151 }
152 Value::Array(arr) => {
153 for (index, val) in arr.iter().enumerate() {
154 let new_prefix = if prefix.is_empty() {
155 index.to_string()
156 } else {
157 format!("{}.{}", prefix, index)
158 };
159 Self::flatten_json(val, new_prefix, file_path, entries);
160 }
161 }
162 _ => {
163 }
165 }
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use std::io::Write;
173 use tempfile::NamedTempFile;
174
175 #[test]
176 fn test_parse_simple_json() {
177 let mut file = NamedTempFile::new().unwrap();
178 write!(file, r#"{{"key": "value"}}"#).unwrap();
179
180 let entries = JsonParser::parse_file(file.path()).unwrap();
181 assert_eq!(entries.len(), 1);
182 assert_eq!(entries[0].key, "key");
183 assert_eq!(entries[0].value, "value");
184 }
185
186 #[test]
187 fn test_parse_nested_json() {
188 let mut file = NamedTempFile::new().unwrap();
189 write!(file, r#"{{"parent": {{"child": "value"}}}}"#).unwrap();
190
191 let entries = JsonParser::parse_file(file.path()).unwrap();
192 assert_eq!(entries.len(), 1);
193 assert_eq!(entries[0].key, "parent.child");
194 assert_eq!(entries[0].value, "value");
195 }
196
197 #[test]
198 fn test_parse_json_array() {
199 let mut file = NamedTempFile::new().unwrap();
200 write!(file, r#"{{"list": ["item1", "item2"]}}"#).unwrap();
201
202 let entries = JsonParser::parse_file(file.path()).unwrap();
203 assert_eq!(entries.len(), 2);
204
205 let item1 = entries.iter().find(|e| e.value == "item1").unwrap();
207 assert_eq!(item1.key, "list.0");
208
209 let item2 = entries.iter().find(|e| e.value == "item2").unwrap();
211 assert_eq!(item2.key, "list.1");
212 }
213
214 #[test]
215 fn test_bottom_up_trace_json() {
216 let mut file = NamedTempFile::new().unwrap();
217 write!(
218 file,
219 r#"{{
220 "user": {{
221 "authentication": {{
222 "login": "Log In",
223 "logout": "Log Out"
224 }}
225 }}
226}}"#
227 )
228 .unwrap();
229
230 let entries = JsonParser::parse_file_with_query(file.path(), Some("Log In")).unwrap();
231 assert_eq!(entries.len(), 1);
232 assert_eq!(entries[0].value, "Log In");
233 assert!(entries[0].key.contains("login"));
235 }
236}