1use crate::formats::parse_custom_format;
2use crate::transformers::custom_format_to_resource;
3
4use langcodec::{Codec, converter};
5use rayon::prelude::*;
6
7#[derive(Debug, Clone, PartialEq, clap::ValueEnum)]
9pub enum ConflictStrategy {
10 First,
12 Last,
14 Skip,
16}
17
18pub fn run_merge_command(
20 inputs: Vec<String>,
21 output: String,
22 strategy: ConflictStrategy,
23 lang: Option<String>,
24) {
25 if inputs.is_empty() {
26 eprintln!("Error: At least one input file is required.");
27 std::process::exit(1);
28 }
29
30 println!("Reading {} input files...", inputs.len());
32 let read_results: Vec<Result<Codec, String>> = inputs
33 .par_iter()
34 .map(|input| read_input_to_codec(input, lang.clone()))
35 .collect();
36
37 let mut input_codecs: Vec<Codec> = Vec::with_capacity(read_results.len());
38 for (idx, res) in read_results.into_iter().enumerate() {
39 match res {
40 Ok(c) => input_codecs.push(c),
41 Err(e) => {
42 println!("❌ Error reading input file {}/{}", idx + 1, inputs.len());
43 eprintln!("{}", e);
44 std::process::exit(1);
45 }
46 }
47 }
48
49 let mut codec = Codec::from_codecs(input_codecs);
51
52 println!("Merging resources...");
56 let conflict_strategy = match strategy {
57 ConflictStrategy::First => langcodec::types::ConflictStrategy::First,
58 ConflictStrategy::Last => langcodec::types::ConflictStrategy::Last,
59 ConflictStrategy::Skip => langcodec::types::ConflictStrategy::Skip,
60 };
61
62 let merge_count = codec.merge_resources(&conflict_strategy);
63 println!("Merged {} language groups", merge_count);
64
65 println!("Writing merged output...");
66 match converter::infer_format_from_path(output.clone()) {
67 Some(format) => {
68 println!("Converting resources to format: {:?}", format);
69 let source_language = codec
73 .resources
74 .first()
75 .and_then(|r| r.metadata.custom.get("source_language").cloned())
76 .unwrap_or_else(|| {
77 codec
78 .resources
79 .first()
80 .map(|r| r.metadata.language.clone())
81 .unwrap_or("en".to_string())
82 });
83
84 println!("Setting metadata.source_language to: {}", source_language);
85
86 let version = codec
88 .resources
89 .first()
90 .and_then(|r| r.metadata.custom.get("version").cloned())
91 .unwrap_or_else(|| "1.0".to_string());
92
93 println!("Setting metadata.version to: {}", version);
94
95 codec.iter_mut().for_each(|r| {
96 r.metadata
97 .custom
98 .insert("source_language".to_string(), source_language.clone());
99 r.metadata
100 .custom
101 .insert("version".to_string(), version.clone());
102 });
103
104 if let Err(e) = converter::convert_resources_to_format(codec.resources, &output, format)
105 {
106 println!("❌ Error converting resources to format");
107 eprintln!("Error converting to {}: {}", output, e);
108 std::process::exit(1);
109 }
110 }
111 None => {
112 if codec.resources.len() == 1 {
113 println!("Writing single resource to output file");
114 if let Some(resource) = codec.resources.first()
115 && let Err(e) = Codec::write_resource_to_file(resource, &output)
116 {
117 println!("❌ Error writing output file");
118 eprintln!("Error writing to {}: {}", output, e);
119 std::process::exit(1);
120 }
121 } else {
122 println!("❌ Error writing output file");
123 eprintln!("Error writing to {}: multiple resources", output);
124 std::process::exit(1);
125 }
126 }
127 }
128
129 println!(
130 "✅ Successfully merged {} files into {}",
131 inputs.len(),
132 output
133 );
134}
135
136fn read_input_to_resources(
138 input: &str,
139 lang: Option<String>,
140) -> Result<Vec<langcodec::Resource>, String> {
141 {
143 let mut local_codec = Codec::new();
144 if let Ok(()) = local_codec.read_file_by_extension(input, lang.clone()) {
145 return Ok(local_codec.resources);
146 }
147 }
148
149 if input.ends_with(".json") || input.ends_with(".yaml") || input.ends_with(".yml") {
151 crate::validation::validate_custom_format_file(input)
153 .map_err(|e| format!("Failed to validate {}: {}", input, e))?;
154
155 let file_content = std::fs::read_to_string(input)
157 .map_err(|e| format!("Error reading file {}: {}", input, e))?;
158
159 crate::formats::validate_custom_format_content(input, &file_content)
161 .map_err(|e| format!("Invalid custom format {}: {}", input, e))?;
162
163 let resources = custom_format_to_resource(
165 input.to_string(),
166 parse_custom_format("json-language-map")
167 .map_err(|e| format!("Failed to parse custom format: {}", e))?,
168 )
169 .map_err(|e| format!("Failed to convert custom format {}: {}", input, e))?;
170
171 return Ok(resources);
172 }
173
174 Err(format!("Error reading {}: unsupported format", input))
175}
176
177fn read_input_to_codec(input: &str, lang: Option<String>) -> Result<Codec, String> {
179 let resources = read_input_to_resources(input, lang)?;
180 Ok(Codec { resources })
181}