1use crate::formats::{CSVFormat, TSVFormat};
11use crate::{ConflictStrategy, merge_resources};
12use crate::{
13 error::Error,
14 formats::*,
15 traits::Parser,
16 types::{Entry, Resource},
17};
18use std::path::Path;
19
20#[derive(Debug, Clone)]
23pub struct Codec {
24 pub resources: Vec<Resource>,
26}
27
28impl Default for Codec {
29 fn default() -> Self {
30 Codec::new()
31 }
32}
33
34impl Codec {
35 pub fn new() -> Self {
41 Codec {
42 resources: Vec::new(),
43 }
44 }
45
46 pub fn builder() -> crate::builder::CodecBuilder {
67 crate::builder::CodecBuilder::new()
68 }
69
70 pub fn iter(&self) -> std::slice::Iter<'_, Resource> {
72 self.resources.iter()
73 }
74
75 pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, Resource> {
77 self.resources.iter_mut()
78 }
79
80 pub fn get_by_language(&self, lang: &str) -> Option<&Resource> {
82 self.resources
83 .iter()
84 .find(|res| res.metadata.language == lang)
85 }
86
87 pub fn get_mut_by_language(&mut self, lang: &str) -> Option<&mut Resource> {
89 self.resources
90 .iter_mut()
91 .find(|res| res.metadata.language == lang)
92 }
93
94 pub fn add_resource(&mut self, resource: Resource) {
96 self.resources.push(resource);
97 }
98
99 pub fn extend_from(&mut self, mut other: Codec) {
101 self.resources.append(&mut other.resources);
102 }
103
104 pub fn from_codecs<I>(codecs: I) -> Self
106 where
107 I: IntoIterator<Item = Codec>,
108 {
109 let mut combined = Codec::new();
110 for mut c in codecs {
111 combined.resources.append(&mut c.resources);
112 }
113 combined
114 }
115
116 pub fn merge_codecs<I>(codecs: I, strategy: &ConflictStrategy) -> Self
120 where
121 I: IntoIterator<Item = Codec>,
122 {
123 let mut combined = Codec::from_codecs(codecs);
124 let _ = combined.merge_resources(strategy);
125 combined
126 }
127
128 pub fn find_entries(&self, key: &str) -> Vec<(&Resource, &Entry)> {
155 let mut results = Vec::new();
156 for resource in &self.resources {
157 for entry in &resource.entries {
158 if entry.id == key {
159 results.push((resource, entry));
160 }
161 }
162 }
163 results
164 }
165
166 pub fn find_entry(&self, key: &str, language: &str) -> Option<&Entry> {
190 self.get_by_language(language)?
191 .entries
192 .iter()
193 .find(|entry| entry.id == key)
194 }
195
196 pub fn find_entry_mut(&mut self, key: &str, language: &str) -> Option<&mut Entry> {
222 self.get_mut_by_language(language)?
223 .entries
224 .iter_mut()
225 .find(|entry| entry.id == key)
226 }
227
228 pub fn update_translation(
259 &mut self,
260 key: &str,
261 language: &str,
262 translation: crate::types::Translation,
263 status: Option<crate::types::EntryStatus>,
264 ) -> Result<(), Error> {
265 if let Some(entry) = self.find_entry_mut(key, language) {
266 entry.value = translation;
267 if let Some(new_status) = status {
268 entry.status = new_status;
269 }
270 Ok(())
271 } else {
272 Err(Error::InvalidResource(format!(
273 "Entry '{}' not found in language '{}'",
274 key, language
275 )))
276 }
277 }
278
279 pub fn add_entry(
312 &mut self,
313 key: &str,
314 language: &str,
315 translation: crate::types::Translation,
316 comment: Option<String>,
317 status: Option<crate::types::EntryStatus>,
318 ) -> Result<(), Error> {
319 let resource = if let Some(resource) = self.get_mut_by_language(language) {
321 resource
322 } else {
323 let new_resource = crate::types::Resource {
325 metadata: crate::types::Metadata {
326 language: language.to_string(),
327 domain: "".to_string(),
328 custom: std::collections::HashMap::new(),
329 },
330 entries: Vec::new(),
331 };
332 self.add_resource(new_resource);
333 self.get_mut_by_language(language).unwrap()
334 };
335
336 let entry = crate::types::Entry {
337 id: key.to_string(),
338 value: translation,
339 comment,
340 status: status.unwrap_or(crate::types::EntryStatus::New),
341 custom: std::collections::HashMap::new(),
342 };
343 resource.add_entry(entry);
344 Ok(())
345 }
346
347 pub fn remove_entry(&mut self, key: &str, language: &str) -> Result<(), Error> {
372 if let Some(resource) = self.get_mut_by_language(language) {
373 let initial_len = resource.entries.len();
374 resource.entries.retain(|entry| entry.id != key);
375
376 if resource.entries.len() == initial_len {
377 Err(Error::InvalidResource(format!(
378 "Entry '{}' not found in language '{}'",
379 key, language
380 )))
381 } else {
382 Ok(())
383 }
384 } else {
385 Err(Error::InvalidResource(format!(
386 "Language '{}' not found",
387 language
388 )))
389 }
390 }
391
392 pub fn copy_entry(
421 &mut self,
422 key: &str,
423 from_language: &str,
424 to_language: &str,
425 update_status: bool,
426 ) -> Result<(), Error> {
427 let source_entry = self.find_entry(key, from_language).ok_or_else(|| {
428 Error::InvalidResource(format!(
429 "Entry '{}' not found in source language '{}'",
430 key, from_language
431 ))
432 })?;
433
434 let mut new_entry = source_entry.clone();
435 if update_status {
436 new_entry.status = crate::types::EntryStatus::New;
437 }
438
439 let target_resource = if let Some(resource) = self.get_mut_by_language(to_language) {
441 resource
442 } else {
443 let new_resource = crate::types::Resource {
445 metadata: crate::types::Metadata {
446 language: to_language.to_string(),
447 domain: "".to_string(),
448 custom: std::collections::HashMap::new(),
449 },
450 entries: Vec::new(),
451 };
452 self.add_resource(new_resource);
453 self.get_mut_by_language(to_language).unwrap()
454 };
455
456 target_resource.entries.retain(|entry| entry.id != key);
458 target_resource.add_entry(new_entry);
459 Ok(())
460 }
461
462 pub fn languages(&self) -> impl Iterator<Item = &str> {
481 self.resources.iter().map(|r| r.metadata.language.as_str())
482 }
483
484 pub fn all_keys(&self) -> impl Iterator<Item = &str> {
503 use std::collections::HashSet;
504
505 let mut keys = HashSet::new();
506 for resource in &self.resources {
507 for entry in &resource.entries {
508 keys.insert(entry.id.as_str());
509 }
510 }
511 keys.into_iter()
512 }
513
514 pub fn has_entry(&self, key: &str, language: &str) -> bool {
538 self.find_entry(key, language).is_some()
539 }
540
541 pub fn entry_count(&self, language: &str) -> usize {
563 self.get_by_language(language)
564 .map(|r| r.entries.len())
565 .unwrap_or(0)
566 }
567
568 pub fn validate(&self) -> Result<(), Error> {
587 if self.resources.is_empty() {
589 return Err(Error::InvalidResource("No resources found".to_string()));
590 }
591
592 let mut languages = std::collections::HashSet::new();
594 for resource in &self.resources {
595 if !languages.insert(&resource.metadata.language) {
596 return Err(Error::InvalidResource(format!(
597 "Duplicate language found: {}",
598 resource.metadata.language
599 )));
600 }
601 }
602
603 for resource in &self.resources {
605 if resource.entries.is_empty() {
606 return Err(Error::InvalidResource(format!(
607 "Resource for language '{}' has no entries",
608 resource.metadata.language
609 )));
610 }
611 }
612
613 Ok(())
614 }
615
616 pub fn clean_up_resources(&mut self) {
618 self.resources
619 .retain(|resource| !resource.entries.is_empty());
620 }
621
622 pub fn merge_resources(&mut self, strategy: &ConflictStrategy) -> usize {
658 let mut grouped_resources: std::collections::HashMap<String, Vec<Resource>> =
660 std::collections::HashMap::new();
661 for resource in &self.resources {
662 grouped_resources
663 .entry(resource.metadata.language.clone())
664 .or_default()
665 .push(resource.clone());
666 }
667
668 let mut merge_count = 0;
669
670 for (_language, resources) in grouped_resources {
672 if resources.len() > 1 {
673 match merge_resources(&resources, strategy) {
674 Ok(merged) => {
675 self.resources.retain(|r| r.metadata.language != _language);
677 self.resources.push(merged);
678 merge_count += 1;
679 }
680 Err(e) => {
681 panic!("Unexpected error merging resources: {}", e);
685 }
686 }
687 }
688 }
689
690 merge_count
691 }
692
693 pub fn write_resource_to_file(resource: &Resource, output_path: &str) -> Result<(), Error> {
721 use crate::formats::{
722 AndroidStringsFormat, CSVFormat, StringsFormat, TSVFormat, XcstringsFormat,
723 };
724 use std::path::Path;
725
726 let format_type =
728 crate::converter::infer_format_from_extension(output_path).ok_or_else(|| {
729 Error::InvalidResource(format!(
730 "Cannot infer format from output path: {}",
731 output_path
732 ))
733 })?;
734
735 match format_type {
736 crate::formats::FormatType::AndroidStrings(_) => {
737 AndroidStringsFormat::from(resource.clone())
738 .write_to(Path::new(output_path))
739 .map_err(|e| {
740 Error::conversion_error(
741 format!("Error writing AndroidStrings output: {}", e),
742 None,
743 )
744 })
745 }
746 crate::formats::FormatType::Strings(_) => StringsFormat::try_from(resource.clone())
747 .and_then(|f| f.write_to(Path::new(output_path)))
748 .map_err(|e| {
749 Error::conversion_error(format!("Error writing Strings output: {}", e), None)
750 }),
751 crate::formats::FormatType::Xcstrings => {
752 XcstringsFormat::try_from(vec![resource.clone()])
753 .and_then(|f| f.write_to(Path::new(output_path)))
754 .map_err(|e| {
755 Error::conversion_error(
756 format!("Error writing Xcstrings output: {}", e),
757 None,
758 )
759 })
760 }
761 crate::formats::FormatType::CSV => CSVFormat::try_from(vec![resource.clone()])
762 .and_then(|f| f.write_to(Path::new(output_path)))
763 .map_err(|e| {
764 Error::conversion_error(format!("Error writing CSV output: {}", e), None)
765 }),
766 crate::formats::FormatType::TSV => TSVFormat::try_from(vec![resource.clone()])
767 .and_then(|f| f.write_to(Path::new(output_path)))
768 .map_err(|e| {
769 Error::conversion_error(format!("Error writing TSV output: {}", e), None)
770 }),
771 }
772 }
773
774 pub fn read_file_by_type<P: AsRef<Path>>(
785 &mut self,
786 path: P,
787 format_type: FormatType,
788 ) -> Result<(), Error> {
789 let language = crate::converter::infer_language_from_path(&path, &format_type)?;
790
791 let domain = path
792 .as_ref()
793 .file_stem()
794 .and_then(|s| s.to_str())
795 .unwrap_or_default()
796 .to_string();
797 let path = path.as_ref();
798
799 let mut new_resources = match &format_type {
800 FormatType::Strings(_) => {
801 vec![Resource::from(StringsFormat::read_from(path)?)]
802 }
803 FormatType::AndroidStrings(_) => {
804 vec![Resource::from(AndroidStringsFormat::read_from(path)?)]
805 }
806 FormatType::Xcstrings => Vec::<Resource>::try_from(XcstringsFormat::read_from(path)?)?,
807 FormatType::CSV => {
808 let csv_format = CSVFormat::read_from(path)?;
810 Vec::<Resource>::try_from(csv_format)?
811 }
812 FormatType::TSV => {
813 let tsv_format = TSVFormat::read_from(path)?;
815 Vec::<Resource>::try_from(tsv_format)?
816 }
817 };
818
819 for new_resource in &mut new_resources {
820 if let Some(ref lang) = language {
821 new_resource.metadata.language = lang.clone();
822 }
823 new_resource.metadata.domain = domain.clone();
824 new_resource
825 .metadata
826 .custom
827 .insert("format".to_string(), format_type.to_string());
828 }
829 self.resources.append(&mut new_resources);
830
831 Ok(())
832 }
833
834 pub fn read_file_by_extension<P: AsRef<Path>>(
846 &mut self,
847 path: P,
848 lang: Option<String>,
849 ) -> Result<(), Error> {
850 let format_type = match path.as_ref().extension().and_then(|s| s.to_str()) {
851 Some("xml") => FormatType::AndroidStrings(lang),
852 Some("strings") => FormatType::Strings(lang),
853 Some("xcstrings") => FormatType::Xcstrings,
854 Some("csv") => FormatType::CSV,
855 Some("tsv") => FormatType::TSV,
856 extension => {
857 return Err(Error::UnsupportedFormat(format!(
858 "Unsupported file extension: {:?}.",
859 extension
860 )));
861 }
862 };
863
864 self.read_file_by_type(path, format_type)?;
865
866 Ok(())
867 }
868
869 pub fn write_to_file(&self) -> Result<(), Error> {
876 let mut grouped_resources: std::collections::HashMap<String, Vec<Resource>> =
878 std::collections::HashMap::new();
879 for resource in &*self.resources {
880 let domain = resource.metadata.domain.clone();
881 grouped_resources
882 .entry(domain)
883 .or_default()
884 .push(resource.clone());
885 }
886
887 for (domain, resources) in grouped_resources {
889 crate::converter::write_resources_to_file(&resources, &domain)?;
890 }
891
892 Ok(())
893 }
894
895 pub fn cache_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), Error> {
904 let path = path.as_ref();
905 if let Some(parent) = path.parent() {
906 std::fs::create_dir_all(parent).map_err(Error::Io)?;
907 }
908 let mut writer = std::fs::File::create(path).map_err(Error::Io)?;
909 serde_json::to_writer(&mut writer, &*self.resources).map_err(Error::Parse)?;
910 Ok(())
911 }
912
913 pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
922 let mut reader = std::fs::File::open(path).map_err(Error::Io)?;
923 let resources: Vec<Resource> =
924 serde_json::from_reader(&mut reader).map_err(Error::Parse)?;
925 Ok(Codec { resources })
926 }
927}
928
929#[cfg(test)]
930mod tests {
931 use super::*;
932 use crate::types::{Entry, EntryStatus, Metadata, Translation};
933 use std::collections::HashMap;
934
935 #[test]
936 fn test_builder_pattern() {
937 let codec = Codec::builder().build();
939 assert_eq!(codec.resources.len(), 0);
940
941 let resource1 = Resource {
943 metadata: Metadata {
944 language: "en".to_string(),
945 domain: "test".to_string(),
946 custom: std::collections::HashMap::new(),
947 },
948 entries: vec![Entry {
949 id: "hello".to_string(),
950 value: Translation::Singular("Hello".to_string()),
951 comment: None,
952 status: EntryStatus::Translated,
953 custom: std::collections::HashMap::new(),
954 }],
955 };
956
957 let resource2 = Resource {
958 metadata: Metadata {
959 language: "fr".to_string(),
960 domain: "test".to_string(),
961 custom: std::collections::HashMap::new(),
962 },
963 entries: vec![Entry {
964 id: "hello".to_string(),
965 value: Translation::Singular("Bonjour".to_string()),
966 comment: None,
967 status: EntryStatus::Translated,
968 custom: std::collections::HashMap::new(),
969 }],
970 };
971
972 let codec = Codec::builder()
973 .add_resource(resource1.clone())
974 .add_resource(resource2.clone())
975 .build();
976
977 assert_eq!(codec.resources.len(), 2);
978 assert_eq!(codec.resources[0].metadata.language, "en");
979 assert_eq!(codec.resources[1].metadata.language, "fr");
980 }
981
982 #[test]
983 fn test_builder_validation() {
984 let resource_without_language = Resource {
986 metadata: Metadata {
987 language: "".to_string(),
988 domain: "test".to_string(),
989 custom: std::collections::HashMap::new(),
990 },
991 entries: vec![],
992 };
993
994 let result = Codec::builder()
995 .add_resource(resource_without_language)
996 .build_and_validate();
997
998 assert!(result.is_err());
999 assert!(matches!(result.unwrap_err(), Error::Validation(_)));
1000
1001 let resource1 = Resource {
1003 metadata: Metadata {
1004 language: "en".to_string(),
1005 domain: "test".to_string(),
1006 custom: std::collections::HashMap::new(),
1007 },
1008 entries: vec![],
1009 };
1010
1011 let resource2 = Resource {
1012 metadata: Metadata {
1013 language: "en".to_string(), domain: "test".to_string(),
1015 custom: std::collections::HashMap::new(),
1016 },
1017 entries: vec![],
1018 };
1019
1020 let result = Codec::builder()
1021 .add_resource(resource1)
1022 .add_resource(resource2)
1023 .build_and_validate();
1024
1025 assert!(result.is_err());
1026 assert!(matches!(result.unwrap_err(), Error::Validation(_)));
1027 }
1028
1029 #[test]
1030 fn test_builder_add_resources() {
1031 let resources = vec![
1032 Resource {
1033 metadata: Metadata {
1034 language: "en".to_string(),
1035 domain: "test".to_string(),
1036 custom: std::collections::HashMap::new(),
1037 },
1038 entries: vec![],
1039 },
1040 Resource {
1041 metadata: Metadata {
1042 language: "fr".to_string(),
1043 domain: "test".to_string(),
1044 custom: std::collections::HashMap::new(),
1045 },
1046 entries: vec![],
1047 },
1048 ];
1049
1050 let codec = Codec::builder().add_resources(resources).build();
1051 assert_eq!(codec.resources.len(), 2);
1052 assert_eq!(codec.resources[0].metadata.language, "en");
1053 assert_eq!(codec.resources[1].metadata.language, "fr");
1054 }
1055
1056 #[test]
1057 fn test_modification_methods() {
1058 use crate::types::{EntryStatus, Translation};
1059
1060 let mut codec = Codec::new();
1062
1063 let resource1 = Resource {
1065 metadata: Metadata {
1066 language: "en".to_string(),
1067 domain: "test".to_string(),
1068 custom: std::collections::HashMap::new(),
1069 },
1070 entries: vec![Entry {
1071 id: "welcome".to_string(),
1072 value: Translation::Singular("Hello".to_string()),
1073 comment: None,
1074 status: EntryStatus::Translated,
1075 custom: std::collections::HashMap::new(),
1076 }],
1077 };
1078
1079 let resource2 = Resource {
1080 metadata: Metadata {
1081 language: "fr".to_string(),
1082 domain: "test".to_string(),
1083 custom: std::collections::HashMap::new(),
1084 },
1085 entries: vec![Entry {
1086 id: "welcome".to_string(),
1087 value: Translation::Singular("Bonjour".to_string()),
1088 comment: None,
1089 status: EntryStatus::Translated,
1090 custom: std::collections::HashMap::new(),
1091 }],
1092 };
1093
1094 codec.add_resource(resource1);
1095 codec.add_resource(resource2);
1096
1097 let entries = codec.find_entries("welcome");
1099 assert_eq!(entries.len(), 2);
1100 assert_eq!(entries[0].0.metadata.language, "en");
1101 assert_eq!(entries[1].0.metadata.language, "fr");
1102
1103 let entry = codec.find_entry("welcome", "en");
1105 assert!(entry.is_some());
1106 assert_eq!(entry.unwrap().id, "welcome");
1107
1108 if let Some(entry) = codec.find_entry_mut("welcome", "en") {
1110 entry.value = Translation::Singular("Hello, World!".to_string());
1111 entry.status = EntryStatus::NeedsReview;
1112 }
1113
1114 let updated_entry = codec.find_entry("welcome", "en").unwrap();
1116 assert_eq!(updated_entry.value.to_string(), "Hello, World!");
1117 assert_eq!(updated_entry.status, EntryStatus::NeedsReview);
1118
1119 codec
1121 .update_translation(
1122 "welcome",
1123 "fr",
1124 Translation::Singular("Bonjour, le monde!".to_string()),
1125 Some(EntryStatus::NeedsReview),
1126 )
1127 .unwrap();
1128
1129 codec
1131 .add_entry(
1132 "new_key",
1133 "en",
1134 Translation::Singular("New message".to_string()),
1135 Some("A new message".to_string()),
1136 Some(EntryStatus::New),
1137 )
1138 .unwrap();
1139
1140 assert!(codec.has_entry("new_key", "en"));
1141 assert_eq!(codec.entry_count("en"), 2);
1142
1143 codec.remove_entry("new_key", "en").unwrap();
1145 assert!(!codec.has_entry("new_key", "en"));
1146 assert_eq!(codec.entry_count("en"), 1);
1147
1148 codec.copy_entry("welcome", "en", "fr", true).unwrap();
1150 let copied_entry = codec.find_entry("welcome", "fr").unwrap();
1151 assert_eq!(copied_entry.status, EntryStatus::New);
1152
1153 let languages: Vec<_> = codec.languages().collect();
1155 assert_eq!(languages.len(), 2);
1156 assert!(languages.contains(&"en"));
1157 assert!(languages.contains(&"fr"));
1158
1159 let keys: Vec<_> = codec.all_keys().collect();
1161 assert_eq!(keys.len(), 1);
1162 assert!(keys.contains(&"welcome"));
1163 }
1164
1165 #[test]
1166 fn test_validation() {
1167 let mut codec = Codec::new();
1168
1169 let resource_without_language = Resource {
1171 metadata: Metadata {
1172 language: "".to_string(),
1173 domain: "test".to_string(),
1174 custom: std::collections::HashMap::new(),
1175 },
1176 entries: vec![],
1177 };
1178
1179 codec.add_resource(resource_without_language);
1180 assert!(codec.validate().is_err());
1181
1182 let mut codec = Codec::new();
1184 let resource1 = Resource {
1185 metadata: Metadata {
1186 language: "en".to_string(),
1187 domain: "test".to_string(),
1188 custom: std::collections::HashMap::new(),
1189 },
1190 entries: vec![],
1191 };
1192
1193 let resource2 = Resource {
1194 metadata: Metadata {
1195 language: "en".to_string(), domain: "test".to_string(),
1197 custom: std::collections::HashMap::new(),
1198 },
1199 entries: vec![],
1200 };
1201
1202 codec.add_resource(resource1);
1203 codec.add_resource(resource2);
1204 assert!(codec.validate().is_err());
1205
1206 let mut codec = Codec::new();
1208 let resource1 = Resource {
1209 metadata: Metadata {
1210 language: "en".to_string(),
1211 domain: "test".to_string(),
1212 custom: std::collections::HashMap::new(),
1213 },
1214 entries: vec![Entry {
1215 id: "welcome".to_string(),
1216 value: Translation::Singular("Hello".to_string()),
1217 comment: None,
1218 status: EntryStatus::Translated,
1219 custom: std::collections::HashMap::new(),
1220 }],
1221 };
1222
1223 let resource2 = Resource {
1224 metadata: Metadata {
1225 language: "fr".to_string(),
1226 domain: "test".to_string(),
1227 custom: std::collections::HashMap::new(),
1228 },
1229 entries: vec![], };
1231
1232 codec.add_resource(resource1);
1233 codec.add_resource(resource2);
1234 assert!(codec.validate().is_err());
1235 }
1236
1237 #[test]
1238 fn test_convert_csv_to_xcstrings() {
1239 let temp_dir = tempfile::tempdir().unwrap();
1241 let input_file = temp_dir.path().join("test.csv");
1242 let output_file = temp_dir.path().join("output.xcstrings");
1243
1244 let csv_content =
1245 "key,en,fr,de\nhello,Hello,Bonjour,Hallo\nbye,Goodbye,Au revoir,Auf Wiedersehen\n";
1246 std::fs::write(&input_file, csv_content).unwrap();
1247
1248 let csv_format = CSVFormat::read_from(&input_file).unwrap();
1250 let resources = Vec::<Resource>::try_from(csv_format).unwrap();
1251 println!("CSV parsed to {} resources:", resources.len());
1252 for (i, resource) in resources.iter().enumerate() {
1253 println!(
1254 " Resource {}: language={}, entries={}",
1255 i,
1256 resource.metadata.language,
1257 resource.entries.len()
1258 );
1259 for entry in &resource.entries {
1260 println!(" Entry: id={}, value={:?}", entry.id, entry.value);
1261 }
1262 }
1263
1264 let result = crate::converter::convert(
1265 &input_file,
1266 FormatType::CSV,
1267 &output_file,
1268 FormatType::Xcstrings,
1269 );
1270
1271 match result {
1272 Ok(()) => println!("✅ CSV to XCStrings conversion succeeded"),
1273 Err(e) => println!("❌ CSV to XCStrings conversion failed: {}", e),
1274 }
1275
1276 if output_file.exists() {
1278 let content = std::fs::read_to_string(&output_file).unwrap();
1279 println!("Output file content: {}", content);
1280 }
1281
1282 let _ = std::fs::remove_file(input_file);
1284 let _ = std::fs::remove_file(output_file);
1285 }
1286
1287 #[test]
1288 fn test_merge_resources_method() {
1289 use crate::types::{ConflictStrategy, Entry, EntryStatus, Metadata, Translation};
1290
1291 let mut codec = Codec::new();
1292
1293 let resource1 = Resource {
1295 metadata: Metadata {
1296 language: "en".to_string(),
1297 domain: "domain1".to_string(),
1298 custom: HashMap::new(),
1299 },
1300 entries: vec![Entry {
1301 id: "hello".to_string(),
1302 value: Translation::Singular("Hello".to_string()),
1303 comment: None,
1304 status: EntryStatus::Translated,
1305 custom: HashMap::new(),
1306 }],
1307 };
1308
1309 let resource2 = Resource {
1310 metadata: Metadata {
1311 language: "en".to_string(),
1312 domain: "domain2".to_string(),
1313 custom: HashMap::new(),
1314 },
1315 entries: vec![Entry {
1316 id: "goodbye".to_string(),
1317 value: Translation::Singular("Goodbye".to_string()),
1318 comment: None,
1319 status: EntryStatus::Translated,
1320 custom: HashMap::new(),
1321 }],
1322 };
1323
1324 let resource3 = Resource {
1325 metadata: Metadata {
1326 language: "en".to_string(),
1327 domain: "domain3".to_string(),
1328 custom: HashMap::new(),
1329 },
1330 entries: vec![Entry {
1331 id: "hello".to_string(), value: Translation::Singular("Hi".to_string()),
1333 comment: None,
1334 status: EntryStatus::Translated,
1335 custom: HashMap::new(),
1336 }],
1337 };
1338
1339 codec.add_resource(resource1);
1341 codec.add_resource(resource2);
1342 codec.add_resource(resource3);
1343
1344 let merges_performed = codec.merge_resources(&ConflictStrategy::Last);
1346 assert_eq!(merges_performed, 1); assert_eq!(codec.resources.len(), 1); let merged_resource = &codec.resources[0];
1350 assert_eq!(merged_resource.metadata.language, "en");
1351 assert_eq!(merged_resource.entries.len(), 2); let hello_entry = merged_resource
1355 .entries
1356 .iter()
1357 .find(|e| e.id == "hello")
1358 .unwrap();
1359 assert_eq!(hello_entry.value.plain_translation_string(), "Hi");
1360 }
1361
1362 #[test]
1363 fn test_merge_resources_method_multiple_languages() {
1364 use crate::types::{ConflictStrategy, Entry, EntryStatus, Metadata, Translation};
1365
1366 let mut codec = Codec::new();
1367
1368 let en_resource1 = Resource {
1370 metadata: Metadata {
1371 language: "en".to_string(),
1372 domain: "domain1".to_string(),
1373 custom: HashMap::new(),
1374 },
1375 entries: vec![Entry {
1376 id: "hello".to_string(),
1377 value: Translation::Singular("Hello".to_string()),
1378 comment: None,
1379 status: EntryStatus::Translated,
1380 custom: HashMap::new(),
1381 }],
1382 };
1383
1384 let en_resource2 = Resource {
1385 metadata: Metadata {
1386 language: "en".to_string(),
1387 domain: "domain2".to_string(),
1388 custom: HashMap::new(),
1389 },
1390 entries: vec![Entry {
1391 id: "goodbye".to_string(),
1392 value: Translation::Singular("Goodbye".to_string()),
1393 comment: None,
1394 status: EntryStatus::Translated,
1395 custom: HashMap::new(),
1396 }],
1397 };
1398
1399 let fr_resource = Resource {
1401 metadata: Metadata {
1402 language: "fr".to_string(),
1403 domain: "domain1".to_string(),
1404 custom: HashMap::new(),
1405 },
1406 entries: vec![Entry {
1407 id: "bonjour".to_string(),
1408 value: Translation::Singular("Bonjour".to_string()),
1409 comment: None,
1410 status: EntryStatus::Translated,
1411 custom: HashMap::new(),
1412 }],
1413 };
1414
1415 codec.add_resource(en_resource1);
1417 codec.add_resource(en_resource2);
1418 codec.add_resource(fr_resource);
1419
1420 let merges_performed = codec.merge_resources(&ConflictStrategy::First);
1422 assert_eq!(merges_performed, 1); assert_eq!(codec.resources.len(), 2); let en_resource = codec.get_by_language("en").unwrap();
1427 assert_eq!(en_resource.entries.len(), 2);
1428
1429 let fr_resource = codec.get_by_language("fr").unwrap();
1431 assert_eq!(fr_resource.entries.len(), 1);
1432 assert_eq!(fr_resource.entries[0].id, "bonjour");
1433 }
1434
1435 #[test]
1436 fn test_merge_resources_method_no_merges() {
1437 use crate::types::{ConflictStrategy, Entry, EntryStatus, Metadata, Translation};
1438
1439 let mut codec = Codec::new();
1440
1441 let en_resource = Resource {
1443 metadata: Metadata {
1444 language: "en".to_string(),
1445 domain: "domain1".to_string(),
1446 custom: HashMap::new(),
1447 },
1448 entries: vec![Entry {
1449 id: "hello".to_string(),
1450 value: Translation::Singular("Hello".to_string()),
1451 comment: None,
1452 status: EntryStatus::Translated,
1453 custom: HashMap::new(),
1454 }],
1455 };
1456
1457 let fr_resource = Resource {
1458 metadata: Metadata {
1459 language: "fr".to_string(),
1460 domain: "domain1".to_string(),
1461 custom: HashMap::new(),
1462 },
1463 entries: vec![Entry {
1464 id: "bonjour".to_string(),
1465 value: Translation::Singular("Bonjour".to_string()),
1466 comment: None,
1467 status: EntryStatus::Translated,
1468 custom: HashMap::new(),
1469 }],
1470 };
1471
1472 codec.add_resource(en_resource);
1474 codec.add_resource(fr_resource);
1475
1476 let merges_performed = codec.merge_resources(&ConflictStrategy::Last);
1478 assert_eq!(merges_performed, 0); assert_eq!(codec.resources.len(), 2); assert!(codec.get_by_language("en").is_some());
1483 assert!(codec.get_by_language("fr").is_some());
1484 }
1485
1486 #[test]
1487 fn test_merge_resources_method_empty_codec() {
1488 let mut codec = Codec::new();
1489
1490 let merges_performed = codec.merge_resources(&ConflictStrategy::Last);
1492 assert_eq!(merges_performed, 0);
1493 assert_eq!(codec.resources.len(), 0);
1494 }
1495
1496 #[test]
1497 fn test_extend_from_and_from_codecs() {
1498 let mut codec1 = Codec::new();
1499 let mut codec2 = Codec::new();
1500
1501 let en_resource = Resource {
1502 metadata: Metadata {
1503 language: "en".to_string(),
1504 domain: "d1".to_string(),
1505 custom: HashMap::new(),
1506 },
1507 entries: vec![Entry {
1508 id: "hello".to_string(),
1509 value: Translation::Singular("Hello".to_string()),
1510 comment: None,
1511 status: EntryStatus::Translated,
1512 custom: HashMap::new(),
1513 }],
1514 };
1515
1516 let fr_resource = Resource {
1517 metadata: Metadata {
1518 language: "fr".to_string(),
1519 domain: "d2".to_string(),
1520 custom: HashMap::new(),
1521 },
1522 entries: vec![Entry {
1523 id: "bonjour".to_string(),
1524 value: Translation::Singular("Bonjour".to_string()),
1525 comment: None,
1526 status: EntryStatus::Translated,
1527 custom: HashMap::new(),
1528 }],
1529 };
1530
1531 codec1.add_resource(en_resource);
1532 codec2.add_resource(fr_resource);
1533
1534 let mut combined = codec1;
1536 combined.extend_from(codec2);
1537 assert_eq!(combined.resources.len(), 2);
1538
1539 let c = Codec::from_codecs(vec![combined.clone()]);
1541 assert_eq!(c.resources.len(), 2);
1542 }
1543
1544 #[test]
1545 fn test_merge_codecs_across_instances() {
1546 use crate::types::ConflictStrategy;
1547
1548 let mut c1 = Codec::new();
1550 let mut c2 = Codec::new();
1551
1552 c1.add_resource(Resource {
1553 metadata: Metadata {
1554 language: "en".to_string(),
1555 domain: "d1".to_string(),
1556 custom: HashMap::new(),
1557 },
1558 entries: vec![Entry {
1559 id: "hello".to_string(),
1560 value: Translation::Singular("Hello".to_string()),
1561 comment: None,
1562 status: EntryStatus::Translated,
1563 custom: HashMap::new(),
1564 }],
1565 });
1566
1567 c2.add_resource(Resource {
1568 metadata: Metadata {
1569 language: "en".to_string(),
1570 domain: "d2".to_string(),
1571 custom: HashMap::new(),
1572 },
1573 entries: vec![Entry {
1574 id: "goodbye".to_string(),
1575 value: Translation::Singular("Goodbye".to_string()),
1576 comment: None,
1577 status: EntryStatus::Translated,
1578 custom: HashMap::new(),
1579 }],
1580 });
1581
1582 let merged = Codec::merge_codecs(vec![c1, c2], &ConflictStrategy::Last);
1583 assert_eq!(merged.resources.len(), 1);
1584 assert_eq!(merged.resources[0].metadata.language, "en");
1585 assert_eq!(merged.resources[0].entries.len(), 2);
1586 }
1587}