1use indexmap::IndexMap;
2
3use crate::error::XcStringsError;
4use crate::model::xcstrings::{
5 ExtractionState, Localization, StringEntry, StringUnit, TranslationState, XcStringsFile,
6};
7
8pub fn create_empty_file(source_language: &str) -> Result<XcStringsFile, XcStringsError> {
10 if source_language.is_empty() {
11 return Err(XcStringsError::InvalidFormat(
12 "source_language is empty".into(),
13 ));
14 }
15 Ok(XcStringsFile {
16 source_language: source_language.to_string(),
17 strings: IndexMap::new(),
18 version: "1.0".to_string(),
19 })
20}
21
22pub struct AddKeyRequest {
24 pub key: String,
25 pub source_text: String,
26 pub comment: Option<String>,
27}
28
29pub struct AddKeysResult {
31 pub added: usize,
32 pub skipped: Vec<String>,
33}
34
35pub fn add_keys(file: &mut XcStringsFile, keys: &[AddKeyRequest]) -> AddKeysResult {
37 let source_language = file.source_language.clone();
38 let mut added = 0;
39 let mut skipped = Vec::new();
40
41 for req in keys {
42 if file.strings.contains_key(&req.key) {
43 skipped.push(req.key.clone());
44 continue;
45 }
46
47 let mut localizations = IndexMap::new();
48 localizations.insert(
49 source_language.clone(),
50 Localization {
51 string_unit: Some(StringUnit {
52 state: TranslationState::Translated,
53 value: req.source_text.clone(),
54 }),
55 variations: None,
56 substitutions: None,
57 },
58 );
59
60 let entry = StringEntry {
61 extraction_state: Some(ExtractionState::Manual),
62 should_translate: true,
63 comment: req.comment.clone(),
64 localizations: Some(localizations),
65 };
66
67 file.strings.insert(req.key.clone(), entry);
68 added += 1;
69 }
70
71 AddKeysResult { added, skipped }
72}
73
74pub fn update_comments(file: &mut XcStringsFile, updates: &[(String, String)]) -> usize {
77 let mut count = 0;
78 for (key, comment) in updates {
79 if let Some(entry) = file.strings.get_mut(key) {
80 entry.comment = Some(comment.clone());
81 count += 1;
82 }
83 }
84 count
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 #[test]
92 fn create_empty_file_valid() {
93 let file = create_empty_file("en").unwrap();
94 assert_eq!(file.source_language, "en");
95 assert!(file.strings.is_empty());
96 assert_eq!(file.version, "1.0");
97 }
98
99 #[test]
100 fn create_empty_file_empty_source_language() {
101 let result = create_empty_file("");
102 assert!(matches!(
103 result.unwrap_err(),
104 XcStringsError::InvalidFormat(_)
105 ));
106 }
107
108 #[test]
109 fn add_keys_to_empty_file() {
110 let mut file = create_empty_file("en").unwrap();
111 let keys = vec![AddKeyRequest {
112 key: "greeting".to_string(),
113 source_text: "Hello".to_string(),
114 comment: Some("A greeting".to_string()),
115 }];
116
117 let result = add_keys(&mut file, &keys);
118 assert_eq!(result.added, 1);
119 assert!(result.skipped.is_empty());
120 assert_eq!(file.strings.len(), 1);
121
122 let entry = &file.strings["greeting"];
123 assert_eq!(entry.extraction_state, Some(ExtractionState::Manual));
124 assert!(entry.should_translate);
125 assert_eq!(entry.comment.as_deref(), Some("A greeting"));
126
127 let locs = entry.localizations.as_ref().unwrap();
128 let en = locs.get("en").unwrap().string_unit.as_ref().unwrap();
129 assert_eq!(en.state, TranslationState::Translated);
130 assert_eq!(en.value, "Hello");
131 }
132
133 #[test]
134 fn add_keys_to_existing_file() {
135 let mut file = create_empty_file("en").unwrap();
136 let keys1 = vec![AddKeyRequest {
137 key: "a".to_string(),
138 source_text: "Alpha".to_string(),
139 comment: None,
140 }];
141 add_keys(&mut file, &keys1);
142
143 let keys2 = vec![AddKeyRequest {
144 key: "b".to_string(),
145 source_text: "Beta".to_string(),
146 comment: None,
147 }];
148 let result = add_keys(&mut file, &keys2);
149 assert_eq!(result.added, 1);
150 assert_eq!(file.strings.len(), 2);
151 }
152
153 #[test]
154 fn add_keys_duplicate_skip() {
155 let mut file = create_empty_file("en").unwrap();
156 let keys = vec![
157 AddKeyRequest {
158 key: "dup".to_string(),
159 source_text: "First".to_string(),
160 comment: None,
161 },
162 AddKeyRequest {
163 key: "dup".to_string(),
164 source_text: "Second".to_string(),
165 comment: None,
166 },
167 ];
168
169 let result = add_keys(&mut file, &keys);
170 assert_eq!(result.added, 1);
171 assert_eq!(result.skipped, vec!["dup"]);
172 let locs = file.strings["dup"].localizations.as_ref().unwrap();
174 let en = locs.get("en").unwrap().string_unit.as_ref().unwrap();
175 assert_eq!(en.value, "First");
176 }
177
178 #[test]
179 fn add_keys_empty_list() {
180 let mut file = create_empty_file("en").unwrap();
181 let result = add_keys(&mut file, &[]);
182 assert_eq!(result.added, 0);
183 assert!(result.skipped.is_empty());
184 }
185
186 #[test]
187 fn add_keys_comment_preserved() {
188 let mut file = create_empty_file("en").unwrap();
189 let keys = vec![AddKeyRequest {
190 key: "k".to_string(),
191 source_text: "val".to_string(),
192 comment: Some("My comment".to_string()),
193 }];
194 add_keys(&mut file, &keys);
195 assert_eq!(file.strings["k"].comment.as_deref(), Some("My comment"));
196 }
197
198 #[test]
199 fn update_comments_updates_existing() {
200 let mut file = create_empty_file("en").unwrap();
201 let keys = vec![
202 AddKeyRequest {
203 key: "a".to_string(),
204 source_text: "Alpha".to_string(),
205 comment: None,
206 },
207 AddKeyRequest {
208 key: "b".to_string(),
209 source_text: "Beta".to_string(),
210 comment: Some("old".to_string()),
211 },
212 ];
213 add_keys(&mut file, &keys);
214
215 let updates = vec![
216 ("a".to_string(), "New comment for A".to_string()),
217 ("b".to_string(), "Updated comment for B".to_string()),
218 ("nonexistent".to_string(), "Should be skipped".to_string()),
219 ];
220 let count = update_comments(&mut file, &updates);
221 assert_eq!(count, 2);
222 assert_eq!(
223 file.strings["a"].comment.as_deref(),
224 Some("New comment for A")
225 );
226 assert_eq!(
227 file.strings["b"].comment.as_deref(),
228 Some("Updated comment for B")
229 );
230 }
231
232 #[test]
233 fn update_comments_nonexistent_key_skipped() {
234 let mut file = create_empty_file("en").unwrap();
235 let updates = vec![("missing".to_string(), "comment".to_string())];
236 let count = update_comments(&mut file, &updates);
237 assert_eq!(count, 0);
238 }
239}